Changed all functional components to class components. added better comments, minor optimizations here and there, removed some unfinished posts from blogs

This commit is contained in:
Kim, Jimin 2021-05-20 15:56:41 +09:00
parent c850184bc7
commit 046dd05713
20 changed files with 987 additions and 927 deletions

View file

@ -22,15 +22,8 @@
"sourceType": "module" "sourceType": "module"
}, },
"plugins": ["react", "@typescript-eslint"], "plugins": ["react", "@typescript-eslint"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": ["off"]
}
}
],
"rules": { "rules": {
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"react/jsx-uses-vars": "error", "react/jsx-uses-vars": "error",
"react/no-unknown-property": [ "react/no-unknown-property": [

View file

@ -1,8 +0,0 @@
---
title: death of miracle
author: developomp
date: May 15, 2021
---
Understanding things what we once called magic.
Science.

View file

@ -1,25 +0,0 @@
---
title: guide to arch linux
author: developomp
date: May 13, 2021
---
# Guide to Arch Linux
## Why arch linux?
## Getting started
### Philosophy
### Arch wiki
### Command line
## Installation
## display manager
## window manager
# Conclusion

View file

@ -1,6 +0,0 @@
학교로 갔다
집으로 왔다
일을 한다
죽었다
나는 죽었다
나는 죽었다

View file

@ -1,12 +1,12 @@
import React, { createContext } from "react"
import { BrowserRouter as Router, Switch, Route } from "react-router-dom" import { BrowserRouter as Router, Switch, Route } from "react-router-dom"
import { ThemeProvider, createGlobalStyle } from "styled-components" import { ThemeProvider, createGlobalStyle } from "styled-components"
import { HelmetProvider } from "react-helmet-async" import { HelmetProvider } from "react-helmet-async"
import storage from "local-storage-fallback" import storage from "local-storage-fallback"
import { useState, useEffect } from "react"
import Spinner from "./components/Spinner"
import LanguageContext from "./LanguageContext"
import theming from "./theming" import theming from "./theming"
import Spinner from "./components/Spinner"
import Navbar from "./components/Navbar" import Navbar from "./components/Navbar"
import Footer from "./components/Footer" import Footer from "./components/Footer"
@ -126,124 +126,130 @@ blockquote {
} }
` `
function App() { interface AppProps {}
/**
* Loading interface AppState {
*/ isLoading: boolean
const [isLoading, setLoading] = useState(true) currentTheme: string
currentLanguage: string
}
export const LanguageContext = createContext({
language: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function
toggleLanguage: () => {},
})
export default class App extends React.Component<AppProps, AppState> {
constructor(props) {
super(props)
this.state = {
isLoading: true,
currentTheme: storage.getItem("theme") || "dark", // get theme from storage and set to "dark" mode if not set already
currentLanguage: storage.getItem("lang") || "en", // get language from storage and set to "en" if not set already
}
}
componentDidMount() {
// show loading screen until all fonts are loaded.
// Experimental feature. Not fully supported on all browsers (IE, I'm looking at you).
// https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet
// show loading screen until all fonts are loaded.
// Experimental feature. Not fully supported on all browsers (IE, I'm looking at you).
// https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet
useEffect(() => {
// 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 = () => {
setLoading(false) this.setState({ isLoading: false })
} }
} else { } else {
setLoading(false) this.setState({ isLoading: false })
} }
}, [])
/**
* Theme
*/
const [currentTheme, _setTheme] = useState(
storage.getItem("theme") || "dark" // get theme from storage and set to "dark" mode if not set already
)
// save theme when it is changed
useEffect(() => {
storage.setItem("theme", currentTheme)
}, [currentTheme])
/**
* Language
*/
const [currentLanguage, _setLanguage] = useState(
storage.getItem("lang") || "en" // get language from storage and set to "en" if not set already
)
// save language when it is changed
useEffect(() => {
storage.setItem("lang", currentLanguage)
}, [currentLanguage])
const languageState = {
language: currentLanguage,
toggleLanguage: () => {
// cycle through languages
let setLanguageTo = "en"
if (currentLanguage == "en") setLanguageTo = "kr"
_setLanguage(setLanguageTo)
},
} }
return ( componentDidUpdate(_, prevState) {
<HelmetProvider> if (this.state.currentTheme !== prevState.currentTheme) {
<ThemeProvider // save theme when it is changed
theme={{ storage.setItem("theme", this.state.currentTheme)
currentTheme: currentTheme, }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setTheme: (setThemeTo) => _setTheme(setThemeTo), // make setTheme function available in other components
}}
>
<LanguageContext.Provider value={languageState}>
<GlobalStyle />
<Router>
<Navbar />
<div id="content">
{isLoading ? (
<Spinner
size={200}
color={
currentTheme == "dark"
? theming.dark.color1
: theming.light.color1
}
/>
) : (
<Switch>
<Route
exact
path="/"
component={() => (
<Home howMany={4} title="Home" />
)}
/>
<Route
exact
path="/archives"
component={() => (
<Home title="Archives" />
)}
/>
<Route
exact
path="/portfolio"
component={Portfolio}
/>
<Route
exact
path="/404"
component={NotFound}
/>
<Route
exact
path="/:path*"
component={Page}
/>
</Switch>
)}
</div>
<Footer />
</Router>
</LanguageContext.Provider>
</ThemeProvider>
</HelmetProvider>
)
}
export default App if (this.state.currentLanguage !== prevState.currentLanguage) {
// save language when it is changed
storage.setItem("lang", this.state.currentLanguage)
}
}
render() {
return (
<HelmetProvider>
<ThemeProvider
theme={{
currentTheme: this.state.currentTheme,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setTheme: (setThemeTo) =>
this.setState({ currentTheme: setThemeTo }), // make setTheme function available in other components
}}
>
<LanguageContext.Provider
value={{
language: this.state.currentLanguage,
toggleLanguage: () => {
// cycle through languages
let setLanguageTo = "en"
if (this.state.currentLanguage == "en")
setLanguageTo = "kr"
this.setState({
currentLanguage: setLanguageTo,
})
},
}}
>
<GlobalStyle />
<Router>
<Navbar />
<div id="content">
{this.state.isLoading ? (
<Spinner size={200} />
) : (
<Switch>
<Route
exact
path="/"
component={() => (
<Home
howMany={4}
title="Home"
/>
)}
/>
<Route
exact
path="/archives"
component={() => (
<Home title="Archives" />
)}
/>
<Route
exact
path="/portfolio"
component={Portfolio}
/>
<Route
exact
path="/404"
component={NotFound}
/>
<Route
exact
path="/:path*"
component={Page}
/>
</Switch>
)}
</div>
<Footer />
</Router>
</LanguageContext.Provider>
</ThemeProvider>
</HelmetProvider>
)
}
}

View file

@ -1,11 +0,0 @@
/**
* go to App.tsx and search for `languageState` to see the actual values
*/
import { createContext } from "react"
export default createContext({
language: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function
toggleLanguage: () => {},
})

View file

@ -1,69 +1,75 @@
import React from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faGithub } from "@fortawesome/free-brands-svg-icons" import { faGithub } from "@fortawesome/free-brands-svg-icons"
import styled from "styled-components" import styled from "styled-components"
import theming from "../theming" import theming from "../theming"
const StyledFooter = styled.footer` export default class Footer extends React.Component {
display: flex; StyledFooter = styled.footer`
justify-content: space-between; display: flex;
margin-bottom: 1px; /* footer goes outside the page by 1 px for some reason */ justify-content: space-between;
padding: 50px 10px; margin-bottom: 1px; /* footer goes outside the page by 1 px for some reason */
background-color: white; padding: 50px 10px;
background-color: ${(props) => background-color: white;
theming.theme(props.theme.currentTheme, { background-color: ${(props) =>
light: "white", theming.theme(props.theme.currentTheme, {
dark: "black", light: "white",
})}; dark: "black",
color: ${(props) => })};
theming.theme(props.theme.currentTheme, {
light: "black",
dark: "white",
})};
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.logo {
color: gray;
}
`
const StyledLink = styled.a`
width: 30px;
font-size: 2rem;
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "lightgrey",
dark: "grey",
})};
&:hover {
color: ${(props) => color: ${(props) =>
theming.theme(props.theme.currentTheme, { theming.theme(props.theme.currentTheme, {
light: "black", light: "black",
dark: "white", dark: "white",
})}; })};
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.logo {
color: gray;
}
`
StyledLink = styled.a`
width: 30px;
font-size: 2rem;
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "lightgrey",
dark: "grey",
})};
&:hover {
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "black",
dark: "white",
})};
}
`
StyledStrong = styled.strong`
font-size: 1.1rem;
`
render() {
return (
<this.StyledFooter>
<div className="logo">
Copyright &copy; develo
<this.StyledStrong>p</this.StyledStrong>omp
</div>
<div className="icons">
<this.StyledLink
href="https://github.com/developomp/developomp-site"
target="_blank"
>
<FontAwesomeIcon icon={faGithub} />
</this.StyledLink>
</div>
</this.StyledFooter>
)
} }
`
function Footer() {
return (
<StyledFooter>
<div className="logo">
Copyright &copy; develo<strong>p</strong>omp
</div>
<div className="icons">
<StyledLink
href="https://github.com/developomp/developomp-site"
target="_blank"
>
<FontAwesomeIcon icon={faGithub} />
</StyledLink>
</div>
</StyledFooter>
)
} }
export default Footer

View file

@ -1,48 +1,55 @@
import React from "react"
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"
import styled from "styled-components" import styled from "styled-components"
import ReactTooltip from "react-tooltip" import ReactTooltip from "react-tooltip"
import LanguageContext from "../LanguageContext" import { LanguageContext } from "../App"
import theming from "../theming" import theming from "../theming"
const StyledThemeButton = styled.div<{ language: string }>` interface StyledThemeButtonProps {
${theming.styles.navbarButtonStyle} language: string
${(props) => }
props.language == "en"
? "" export default class LanguageToggleButton extends React.Component {
: "transform: scaleX(-1);\ StyledThemeButton = styled.div<StyledThemeButtonProps>`
${theming.styles.navbarButtonStyle}
${(props) =>
props.language == "en"
? ""
: "transform: scaleX(-1);\
-moz-transform: scaleX(-1);\ -moz-transform: scaleX(-1);\
-webkit-transform: scaleX(-1);\ -webkit-transform: scaleX(-1);\
-ms-transform: scaleX(-1);"}; -ms-transform: scaleX(-1);"};
` `
function LanguageToggleButton() { languageName = (language) => {
function languageName(language) {
let name = "English" let name = "English"
if (language == "kr") name = "Korean" if (language == "kr") name = "Korean"
return name return name
} }
return ( render() {
<LanguageContext.Consumer> return (
{({ language, toggleLanguage }) => ( <LanguageContext.Consumer>
<> {({ language, toggleLanguage }) => (
<StyledThemeButton <>
data-tip <this.StyledThemeButton
data-for="language" data-tip
onClick={toggleLanguage} data-for="language"
language={language} onClick={toggleLanguage}
> language={language}
<FontAwesomeIcon icon={faLanguage} /> >
</StyledThemeButton> <FontAwesomeIcon icon={faLanguage} />
<ReactTooltip id="language" type="dark" effect="solid"> </this.StyledThemeButton>
<span>Using {languageName(language)} language</span> <ReactTooltip id="language" type="dark" effect="solid">
</ReactTooltip> <span>
</> Using {this.languageName(language)} language
)} </span>
</LanguageContext.Consumer> </ReactTooltip>
) </>
)}
</LanguageContext.Consumer>
)
}
} }
export default LanguageToggleButton

View file

@ -1,3 +1,4 @@
import React from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faGithub } from "@fortawesome/free-brands-svg-icons" import { faGithub } from "@fortawesome/free-brands-svg-icons"
import styled from "styled-components" import styled from "styled-components"
@ -11,85 +12,84 @@ import Sidebar from "./Sidebar"
import ThemeToggleButton from "./ThemeToggleButton" import ThemeToggleButton from "./ThemeToggleButton"
import LanguageToggleButton from "./LanguageToggleButton" import LanguageToggleButton from "./LanguageToggleButton"
const StyledNav = styled.nav` export default class Navbar extends React.Component {
display: flex; StyledNav = styled.nav`
align-items: center; display: flex;
height: 2rem; align-items: center;
margin: 0; height: 2rem;
padding: 1rem; margin: 0;
background-color: ${(props) => padding: 1rem;
theming.theme(props.theme.currentTheme, { background-color: ${(props) =>
light: theming.light.backgroundColor0, theming.theme(props.theme.currentTheme, {
dark: theming.dark.backgroundColor0, light: theming.light.backgroundColor0,
})}; dark: theming.dark.backgroundColor0,
color: ${(props) => })};
theming.theme(props.theme.currentTheme, { color: ${(props) =>
light: theming.light.color0, theming.theme(props.theme.currentTheme, {
dark: theming.dark.color0, light: theming.light.color0,
})}; dark: theming.dark.color0,
box-shadow: 0 4px 10px rgb(0 0 0 / 5%); })};
box-shadow: 0 4px 10px rgb(0 0 0 / 5%);
.right { .right {
margin-left: auto; margin-left: auto;
}
`
StyledNavLinks = styled.div`
@media only screen and (max-width: ${theming.size.screen_size1}) {
display: none;
}
`
StyledImg = styled.img`
height: 2rem;
margin: 1rem;
`
StyledLink = styled(Link)`
${theming.styles.navbarButtonStyle}
`
StyledALink = styled.a`
${theming.styles.navbarButtonStyle}
`
render() {
return (
<this.StyledNav>
<Link to="/">
<this.StyledImg
src={process.env.PUBLIC_URL + "/icon/icon_circle.svg"}
/>
</Link>
<this.StyledNavLinks>
{NavbarData.map((item, index) => {
return (
<this.StyledLink key={index} to={item.path}>
{item.title}
</this.StyledLink>
)
})}
</this.StyledNavLinks>
<ThemeToggleButton />
<LanguageToggleButton />
<this.StyledALink
data-tip
data-for="github"
href="https://github.com/developomp/developomp-site"
target="_blank"
>
<FontAwesomeIcon icon={faGithub} />
</this.StyledALink>
<ReactTooltip id="github" type="dark" effect="solid">
<span>View source code</span>
</ReactTooltip>
<SearchBox />
<Sidebar />
</this.StyledNav>
)
} }
`
const StyledNavLinks = styled.div`
@media only screen and (max-width: ${theming.size.screen_size1}) {
display: none;
}
`
const StyledImg = styled.img`
height: 2rem;
margin: 1rem;
`
const StyledLink = styled(Link)`
${theming.styles.navbarButtonStyle}
`
const StyledALink = styled.a`
${theming.styles.navbarButtonStyle}
`
function Navbar() {
return (
<StyledNav>
<Link to="/">
<StyledImg
src={process.env.PUBLIC_URL + "/icon/icon_circle.svg"}
/>
</Link>
<StyledNavLinks>
{NavbarData.map((item, index) => {
return (
<StyledLink key={index} to={item.path}>
{item.title}
</StyledLink>
)
})}
</StyledNavLinks>
<ThemeToggleButton />
<LanguageToggleButton />
<StyledALink
data-tip
data-for="github"
href="https://github.com/developomp/developomp-site"
target="_blank"
>
<FontAwesomeIcon icon={faGithub} />
</StyledALink>
<ReactTooltip id="github" type="dark" effect="solid">
<span>View source code</span>
</ReactTooltip>
<SearchBox />
<Sidebar />
</StyledNav>
)
} }
export default Navbar

View file

@ -1,54 +1,60 @@
import React from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faSearch } from "@fortawesome/free-solid-svg-icons" import { faSearch } from "@fortawesome/free-solid-svg-icons"
import styled from "styled-components" import styled from "styled-components"
import theming from "../theming" import theming from "../theming"
const StyledSearchBoxContainer = styled.div` export default class Navbar extends React.Component {
display: flex; StyledSearchBoxContainer = styled.div`
justify-content: left; display: flex;
align-items: center; justify-content: left;
background-color: ${(props) => margin: 0.5rem;
theming.theme(props.theme.currentTheme, { align-items: center;
light: "white",
dark: "#202225",
})};
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "black",
dark: "#CFD0D0",
})};
&:hover {
background-color: ${(props) => background-color: ${(props) =>
theming.theme(props.theme.currentTheme, { theming.theme(props.theme.currentTheme, {
light: "whitesmoke", light: "white",
dark: "#36393F", dark: "#2F3136",
})}; })};
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "black",
dark: "#CFD0D0",
})};
&:hover {
background-color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "whitesmoke",
dark: "#36393F",
})};
}
`
StyledSearchBox = styled.input`
width: 80%;
border: none;
border-right: 1rem;
outline: none;
padding: 10px 10px;
text-decoration: none;
background-color: inherit;
color: inherit;
`
StyledSearchButton = styled(FontAwesomeIcon)`
cursor: pointer;
`
render() {
return (
<this.StyledSearchBoxContainer>
<this.StyledSearchBox
type="text"
name="search"
placeholder="Search"
/>
<this.StyledSearchButton icon={faSearch} />
</this.StyledSearchBoxContainer>
)
} }
`
const StyledSearchBox = styled.input`
width: 80%;
border: none;
border-right: 1rem;
outline: none;
padding: 10px 10px;
text-decoration: none;
background-color: inherit;
color: inherit;
`
const StyledSearchButton = styled(FontAwesomeIcon)`
cursor: pointer;
`
function Navbar() {
return (
<StyledSearchBoxContainer>
<StyledSearchBox type="text" name="search" placeholder="Search" />
<StyledSearchButton icon={faSearch} />
</StyledSearchBoxContainer>
)
} }
export default Navbar

View file

@ -1,6 +1,6 @@
import { useState } from "react" import React from "react"
import styled, { css } from "styled-components" import styled, { css } from "styled-components"
import NavbarData from "../data/NavbarData" import NavbarData, { Item } from "../data/NavbarData"
import ReactTooltip from "react-tooltip" import ReactTooltip from "react-tooltip"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
@ -9,117 +9,130 @@ import { faEllipsisV, faTimes } from "@fortawesome/free-solid-svg-icons"
import theming from "../theming" import theming from "../theming"
import SubMenu from "./SubMenu" import SubMenu from "./SubMenu"
interface StateProps { interface SidebarProps {}
interface SidebarState {
isSidebarOpen: boolean isSidebarOpen: boolean
} }
const CommonSidebarToggleButtonStyle = css` export default class Sidebar extends React.Component<
${theming.styles.navbarButtonStyle} SidebarProps,
font-size: "1.5rem"; SidebarState
width: 1.5rem; > {
text-align: center; constructor(props) {
cursor: pointer; super(props)
margin: 0.1rem; this.state = {
@media only screen and (min-width: ${theming.size.screen_size1}) { isSidebarOpen: false,
display: none; }
}
`
const StyledToggleSidebarButton = styled.div`
${CommonSidebarToggleButtonStyle}
`
const StyledToggleSidebarButton2 = styled.div`
${CommonSidebarToggleButtonStyle}
border-radius: 0;
margin: auto;
width: 90%;
height: 2rem;
font-size: 1.1rem;
`
const StyledOverlay = styled.div<StateProps>`
display: ${(props) => (props.isSidebarOpen ? "block" : "none")};
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20;
transition-property: opacity;
background-color: rgba(0, 0, 0, 25%);
* {
overflow: scroll;
}
`
const SidebarNav = styled.nav<StateProps>`
width: 250px;
height: 100vh;
display: flex;
justify-content: center;
position: fixed;
top: 0;
right: ${(props) => (props.isSidebarOpen ? "0" : "-100%")};
transition: 350ms;
z-index: 30;
overflow-x: hidden;
overflow-y: scroll;
background-color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: theming.light.backgroundColor0,
dark: theming.dark.backgroundColor0,
})};
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: theming.light.color0,
dark: theming.dark.color0,
})};
`
const SidebarWrap = styled.div`
width: 100%;
`
const Sidebar = () => {
const [isSidebarOpen, setSidebar] = useState(false)
function toggleSidebar() {
setSidebar(!isSidebarOpen)
document.body.style.overflow = isSidebarOpen ? "scroll" : "hidden"
} }
return ( // for some reason this.setState only works if this is an arrow function
<> toggleSidebar = () => {
<StyledOverlay this.setState({ isSidebarOpen: !this.state.isSidebarOpen })
isSidebarOpen={isSidebarOpen} document.body.style.overflow = this.state.isSidebarOpen
onClick={toggleSidebar} ? "scroll"
/> : "hidden"
}
<StyledToggleSidebarButton CommonSidebarToggleButtonStyle = css`
data-tip ${theming.styles.navbarButtonStyle}
data-for="sidebar" width: 1.5rem;
onClick={toggleSidebar} text-align: center;
> cursor: pointer;
<FontAwesomeIcon icon={faEllipsisV}></FontAwesomeIcon> @media only screen and (min-width: ${theming.size.screen_size1}) {
<ReactTooltip id="sidebar" type="dark" effect="solid"> display: none;
<span>open sidebar</span> }
</ReactTooltip> `
</StyledToggleSidebarButton>
<SidebarNav isSidebarOpen={isSidebarOpen}> StyledToggleSidebarButton = styled.div`
<SidebarWrap> ${this.CommonSidebarToggleButtonStyle}
<StyledToggleSidebarButton2 onClick={toggleSidebar}> `
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon> Close
</StyledToggleSidebarButton2> StyledToggleSidebarButton2 = styled.div`
{NavbarData.map((item, index) => { ${this.CommonSidebarToggleButtonStyle}
return <SubMenu item={item} key={index} /> border-radius: 0;
})} margin: auto;
</SidebarWrap> width: 90%;
</SidebarNav> height: 2rem;
</> font-size: 1.1rem;
) `
StyledOverlay = styled.div<SidebarState>`
display: ${(props) => (props.isSidebarOpen ? "block" : "none")};
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20;
transition-property: opacity;
background-color: rgba(0, 0, 0, 25%);
* {
overflow: scroll;
}
`
SidebarNav = styled.nav<SidebarState>`
width: 250px;
height: 100vh;
display: flex;
justify-content: center;
position: fixed;
top: 0;
right: ${(props) => (props.isSidebarOpen ? "0" : "-100%")};
transition: 350ms;
z-index: 30;
overflow-x: hidden;
overflow-y: scroll;
background-color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: theming.light.backgroundColor0,
dark: theming.dark.backgroundColor0,
})};
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: theming.light.color0,
dark: theming.dark.color0,
})};
`
SidebarWrap = styled.div`
width: 100%;
`
render() {
return (
<>
<this.StyledOverlay
isSidebarOpen={this.state.isSidebarOpen}
onClick={this.toggleSidebar}
/>
<this.StyledToggleSidebarButton
data-tip
data-for="sidebar"
onClick={this.toggleSidebar}
>
<FontAwesomeIcon icon={faEllipsisV}></FontAwesomeIcon>
<ReactTooltip id="sidebar" type="dark" effect="solid">
<span>open sidebar</span>
</ReactTooltip>
</this.StyledToggleSidebarButton>
<this.SidebarNav isSidebarOpen={this.state.isSidebarOpen}>
<this.SidebarWrap>
<this.StyledToggleSidebarButton2
onClick={this.toggleSidebar}
>
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>{" "}
Close
</this.StyledToggleSidebarButton2>
{NavbarData.map((item: Item, index) => {
return <SubMenu item={item} key={index} />
})}
</this.SidebarWrap>
</this.SidebarNav>
</>
)
}
} }
export default Sidebar

View file

@ -2,115 +2,116 @@
* inspired by https://github.com/dmitrymorozoff/react-spinners-kit/tree/master/src/components/whisper * inspired by https://github.com/dmitrymorozoff/react-spinners-kit/tree/master/src/components/whisper
*/ */
import React from "react"
import styled, { keyframes } from "styled-components" import styled, { keyframes } from "styled-components"
const motion = keyframes`
0% {
transform: scale(1, 1);
opacity: 1;
}
100% {
transform: scale(0, 0);
opacity: 0;
}
`
const spin = keyframes`
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
`
interface BallInterface { interface BallInterface {
color: string
size: number size: number
key: string key: string
index: number index: number
} }
const Ball = styled.div<BallInterface>`
float: left;
clear: right;
margin: ${(props) => props.size / 15}px;
width: ${(props) => props.size / 5}px;
height: ${(props) => props.size / 5}px;
border-radius: 2px;
background-color: ${(props) => props.color};
animation-name: ${motion};
animation-direction: alternate;
animation-duration: 800ms;
animation-iteration-count: infinite;
&:nth-child(1) {
animation-delay: 200ms;
}
&:nth-child(2) {
animation-delay: 400ms;
}
&:nth-child(3) {
animation-delay: 600ms;
}
&:nth-child(4) {
animation-delay: 400ms;
}
&:nth-child(5) {
animation-delay: 600ms;
}
&:nth-child(6) {
animation-delay: 800ms;
}
&:nth-child(7) {
animation-delay: 600ms;
}
&:nth-child(8) {
animation-delay: 800ms;
}
&:nth-child(9) {
animation-delay: 1000ms;
}
`
interface WrapperInterface { interface WrapperInterface {
size: number size: number
} }
const Wrapper = styled.div<WrapperInterface>` interface SpinnerProps {
margin: 5rem auto 0 auto; size: number
width: 200px; }
height: 200px;
animation: ${spin} 10s infinite;
`
function Spinner({ size, color }) { export default class Spinner extends React.Component<SpinnerProps> {
const balls: unknown[] = [] motion = keyframes`
let keyValue = 0 from {
const countBallsInLine = 3 transform: scale(1, 1);
for (let i = 0; i < countBallsInLine; i++) { opacity: 1;
for (let j = 0; j < countBallsInLine; j++) { }
balls.push( to {
<Ball transform: scale(0, 0);
color={color} opacity: 0;
size={size} }
key={keyValue.toString()} `
index={keyValue}
/> spin = keyframes`
) from {
keyValue++ transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
Ball = styled.div<BallInterface>`
float: left;
clear: right;
margin: ${(props) => props.size / 15}px;
width: ${(props) => props.size / 5}px;
height: ${(props) => props.size / 5}px;
border-radius: 2px;
background-color: lightgrey;
animation-name: ${this.motion};
animation-direction: alternate;
animation-duration: 800ms;
animation-timing-function: linear;
animation-iteration-count: infinite;
/* use for loops here? */
&:nth-child(1) {
animation-delay: 200ms;
}
&:nth-child(2) {
animation-delay: 400ms;
}
&:nth-child(3) {
animation-delay: 600ms;
}
&:nth-child(4) {
animation-delay: 400ms;
}
&:nth-child(5) {
animation-delay: 600ms;
}
&:nth-child(6) {
animation-delay: 800ms;
}
&:nth-child(7) {
animation-delay: 600ms;
}
&:nth-child(8) {
animation-delay: 800ms;
}
&:nth-child(9) {
animation-delay: 1000ms;
}
`
Wrapper = styled.div<WrapperInterface>`
margin: 5rem auto 0 auto;
width: 200px;
height: 200px;
animation-timing-function: linear;
animation: ${this.spin} 10s infinite;
`
balls: unknown[] = []
constructor(props) {
super(props)
let keyValue = 0
const countBallsInLine = 3
for (let i = 0; i < countBallsInLine; i++) {
for (let j = 0; j < countBallsInLine; j++) {
this.balls.push(
<this.Ball
size={this.props.size}
key={keyValue.toString()}
index={keyValue}
/>
)
keyValue++
}
} }
} }
return <Wrapper size={size}>{balls}</Wrapper> render() {
return <this.Wrapper size={this.props.size}>{this.balls}</this.Wrapper>
}
} }
export default Spinner

View file

@ -1,74 +1,100 @@
import { useState } from "react" import React from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import styled from "styled-components" import styled from "styled-components"
import theming from "../theming" import theming from "../theming"
import { Item } from "../data/NavbarData"
const SidebarLink = styled(Link)` interface SubMenuProps {
${theming.styles.navbarButtonStyle}; item: Item
display: flex;
width: 100%;
margin: 0;
border-radius: 0;
justify-content: space-between;
height: 2rem;
align-items: center;
padding: 20px;
list-style: none;
`
const SidebarLabel = styled.span`
margin-left: 16px;
`
const DropdownLink = styled(Link)`
background: #414757;
height: 60px;
padding-left: 3rem;
display: flex;
align-items: center;
text-decoration: none;
color: #f5f5f5;
font-size: 18px;
&:hover {
background: #632ce4;
cursor: pointer;
}
`
function SubMenu({ item }) {
const [isSubNavOpen, setSubNav] = useState(false)
const showSubNav = () => setSubNav(!isSubNavOpen)
return (
<>
<SidebarLink to={item.path} onClick={item.subNav && showSubNav}>
<div>
{item.icon}
<SidebarLabel>{item.title}</SidebarLabel>
</div>
<div>
{item.subNav && isSubNavOpen
? item.iconOpened
: item.subNav
? item.iconClosed
: null}
</div>
</SidebarLink>
{/* not used as of the moment */}
{isSubNavOpen &&
item.subNav.map((item, index) => {
return (
<DropdownLink to={item.path} key={index}>
{item.icon}
<SidebarLabel>{item.title}</SidebarLabel>
</DropdownLink>
)
})}
</>
)
} }
export default SubMenu interface SubMenuState {
isSubNavOpen: boolean
}
export default class SubMenu extends React.Component<
SubMenuProps,
SubMenuState
> {
SidebarLink = styled(Link)`
${theming.styles.navbarButtonStyle};
display: flex;
width: 100%;
margin: 0;
border-radius: 0;
justify-content: space-between;
height: 2rem;
align-items: center;
padding: 20px;
list-style: none;
`
SidebarLabel = styled.span`
margin-left: 16px;
`
DropdownLink = styled(Link)`
background: #414757;
height: 60px;
padding-left: 3rem;
display: flex;
align-items: center;
text-decoration: none;
color: #f5f5f5;
font-size: 18px;
&:hover {
background: #632ce4;
cursor: pointer;
}
`
constructor(props) {
super(props)
this.state = {
isSubNavOpen: false,
}
}
showSubNav = () => this.setState({ isSubNavOpen: !this.state.isSubNavOpen })
render() {
return (
<>
<this.SidebarLink
to={this.props.item.path}
onClick={this.props.item.subNav && this.showSubNav}
>
<div>
{this.props.item.icon}
<this.SidebarLabel>
{this.props.item.title}
</this.SidebarLabel>
</div>
<div>
{this.props.item.subNav && this.state.isSubNavOpen
? this.props.item.iconOpened
: this.props.item.subNav
? this.props.item.iconClosed
: null}
</div>
</this.SidebarLink>
{/* not used as of the moment */}
{this.state.isSubNavOpen && // check if subNav is open
this.props.item.subNav && // check if subNav exists in that item
this.props.item.subNav.map((item, index) => {
// shows all items in subNav
return (
<this.DropdownLink to={item.path} key={index}>
{item.icon}
<this.SidebarLabel>
{item.title}
</this.SidebarLabel>
</this.DropdownLink>
)
})}
</>
)
}
}

View file

@ -1,3 +1,4 @@
import React from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons" import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"
import styled, { ThemeConsumer } from "styled-components" import styled, { ThemeConsumer } from "styled-components"
@ -6,45 +7,46 @@ import ReactTooltip from "react-tooltip"
import theming from "../theming" import theming from "../theming"
const StyledThemeButton = styled.div` export default class Navbar extends React.Component {
${theming.styles.navbarButtonStyle} StyledThemeButton = styled.div`
${(props) => ${theming.styles.navbarButtonStyle}
theming.theme(props.theme.currentTheme, { ${(props) =>
light: "", theming.theme(props.theme.currentTheme, {
dark: "transform: scaleX(-1);\ light: "",
dark: "transform: scaleX(-1);\
-moz-transform: scaleX(-1);\ -moz-transform: scaleX(-1);\
-webkit-transform: scaleX(-1);\ -webkit-transform: scaleX(-1);\
-ms-transform: scaleX(-1);", -ms-transform: scaleX(-1);",
})}; })};
` `
render() {
function Navbar() { return (
return ( <ThemeConsumer>
<ThemeConsumer> {({ currentTheme, setTheme }) => (
{({ currentTheme, setTheme }) => ( <>
<> <this.StyledThemeButton
<StyledThemeButton data-tip
data-tip data-for="theme"
data-for="theme" className="right"
className="right" onClick={() =>
onClick={() => setTheme(
setTheme(currentTheme === "dark" ? "light" : "dark") currentTheme === "dark" ? "light" : "dark"
} )
> }
{currentTheme == "dark" && ( >
<FontAwesomeIcon icon={faMoon} /> {currentTheme == "dark" && (
)} <FontAwesomeIcon icon={faMoon} />
{currentTheme == "light" && ( )}
<FontAwesomeIcon icon={faSun} /> {currentTheme == "light" && (
)} <FontAwesomeIcon icon={faSun} />
</StyledThemeButton> )}
<ReactTooltip id="theme" type="dark" effect="solid"> </this.StyledThemeButton>
<span>Using {currentTheme} theme</span> <ReactTooltip id="theme" type="dark" effect="solid">
</ReactTooltip> <span>Using {currentTheme} theme</span>
</> </ReactTooltip>
)} </>
</ThemeConsumer> )}
) </ThemeConsumer>
)
}
} }
export default Navbar

View file

@ -8,7 +8,17 @@ import {
faHiking, faHiking,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
export default [ // item from sidebar data
export type Item = {
path: string
subNav?: Array<Item>
icon: JSX.Element
title: string
iconOpened?: JSX.Element
iconClosed?: JSX.Element
}
const NavbarData: Array<Item> = [
{ {
title: "Home", title: "Home",
path: "/", path: "/",
@ -40,3 +50,5 @@ export default [
icon: <FontAwesomeIcon icon={faUserTie} />, icon: <FontAwesomeIcon icon={faUserTie} />,
}, },
] ]
export default NavbarData

View file

@ -1,3 +1,4 @@
import React from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import styled from "styled-components" import styled from "styled-components"
import theming from "../theming" import theming from "../theming"
@ -6,115 +7,131 @@ import { Helmet } from "react-helmet-async"
import pages from "../pages.json" import pages from "../pages.json"
const StyledPostList = styled.div` interface HomeProps {
padding-top: 2rem; title?: string
margin: auto; howMany?: number
text-align: center;
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "#111111",
dark: "#EEEEEE",
})};
`
const StyledH1 = styled.h1`
margin-bottom: 20px;
font-weight: 500;
margin: 0;
`
const StyledTitle = styled.h1`
font-size: 2rem;
font-style: bold;
`
const StyledLink = styled(Link)`
text-decoration: none;
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "black",
dark: "white",
})};
&:hover {
text-decoration: underline;
}
`
const StyledPostCard = styled.div`
box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
text-align: left;
margin-bottom: 20px;
padding: 10px 20px;
`
function Home(props) {
const PostCards: Array<unknown> = []
let howMany = props.howMany
const isLimited = Boolean(howMany)
let h1Text = "All posts"
if (isLimited) {
h1Text = `${howMany} recent posts`
}
for (const pagePath in pages) {
if (isLimited && howMany <= 0) continue
const post = pages[pagePath]
post.title = post.meta?.title ? post.meta.title : "Unknown title"
post.date = post.meta?.date ? post.meta.date : "Unknown date"
post.author = post.meta?.author ? post.meta.author : "Unknown author"
PostCards.push(
<StyledPostCard key={pagePath} className="card main-content">
<StyledTitle>
<StyledLink to={pagePath}>{post.title}</StyledLink>
</StyledTitle>
<small>
Published on {post.date} by {post.author}
</small>
<hr />
<div
className="link-color"
dangerouslySetInnerHTML={{
__html: marked(
post.content.split(" ").slice(0, 20).join(" ") +
"..."
),
}}
></div>
<small>
<StyledLink to={pagePath}>Read more</StyledLink>
</small>
</StyledPostCard>
)
howMany--
}
return (
<>
<Helmet>
<title>pomp | {props.title}</title>
<meta property="og:title" content={props.title} />
<meta property="og:type" content="website" />
<meta property="og:url" content="http://developomp.com" />
<meta
property="og:image"
content="http://developomp.com/icon/icon.svg"
/>
<meta property="og:description" content="" />
</Helmet>
<StyledPostList>
<StyledH1>{h1Text}</StyledH1>
<br />
{PostCards}
</StyledPostList>
</>
)
} }
export default Home export default class Home extends React.Component<HomeProps, { lol: boolean }> {
h1Text: string
PostCards: Array<unknown> = []
StyledPostList = styled.div`
padding-top: 2rem;
margin: auto;
text-align: center;
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "#111111",
dark: "#EEEEEE",
})};
`
StyledH1 = styled.h1`
margin-bottom: 20px;
font-weight: 500;
margin: 0;
`
StyledTitle = styled.h1`
font-size: 2rem;
font-style: bold;
`
StyledLink = styled(Link)`
text-decoration: none;
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "black",
dark: "white",
})};
&:hover {
text-decoration: underline;
}
`
StyledPostCard = styled.div`
box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
text-align: left;
margin-bottom: 20px;
padding: 10px 20px;
`
constructor(props) {
super(props)
let howMany = props.howMany
const isLimited = Boolean(howMany)
this.h1Text = isLimited ? "All posts" : `${howMany} recent posts`
for (const pagePath in pages) {
if (isLimited && howMany <= 0) continue
const post = pages[pagePath]
post.title = post.meta?.title ? post.meta.title : "Unknown title"
post.date = post.meta?.date ? post.meta.date : "Unknown date"
post.author = post.meta?.author
? post.meta.author
: "Unknown author"
this.PostCards.push(
<this.StyledPostCard
key={pagePath}
className="card main-content"
>
<this.StyledTitle>
<this.StyledLink to={pagePath}>
{post.title}
</this.StyledLink>
</this.StyledTitle>
<small>
Published on {post.date} by {post.author}
</small>
<hr />
<div
className="link-color"
dangerouslySetInnerHTML={{
__html: marked(
post.content.split(" ").slice(0, 20).join(" ") +
"..."
),
}}
></div>
<small>
<this.StyledLink to={pagePath}>
Read more
</this.StyledLink>
</small>
</this.StyledPostCard>
)
howMany--
}
}
render() {
return (
<>
<Helmet>
<title>pomp | {this.props.title}</title>
<meta property="og:title" content={this.props.title} />
<meta property="og:type" content="website" />
<meta property="og:url" content="http://developomp.com" />
<meta
property="og:image"
content="http://developomp.com/icon/icon.svg"
/>
<meta property="og:description" content="" />
</Helmet>
<this.StyledPostList>
<this.StyledH1>{this.h1Text}</this.StyledH1>
<br />
{this.PostCards}
</this.StyledPostList>
</>
)
}
}

View file

@ -1,43 +1,47 @@
import React from "react"
import styled from "styled-components" import styled from "styled-components"
import theming from "../theming" import theming from "../theming"
import { Helmet } from "react-helmet-async" import { Helmet } from "react-helmet-async"
const StyledNotFound = styled.div` export default class NotFound extends React.Component {
margin: auto; StyledNotFound = styled.div`
margin-top: 2rem; margin: auto;
text-align: center; margin-top: 2rem;
color: ${(props) => text-align: center;
theming.theme(props.theme.currentTheme, { color: ${(props) =>
light: "#111111", theming.theme(props.theme.currentTheme, {
dark: "#EEEEEE", light: "#111111",
})}; dark: "#EEEEEE",
` })};
`
const Styled404 = styled.h1` Styled404 = styled.h1`
font-size: 3rem; font-size: 3rem;
` `
function NotFound() { render() {
return ( return (
<> <>
<Helmet> <Helmet>
<title>pomp | 404</title> <title>pomp | 404</title>
<meta property="og:title" content="Page Not Found" /> <meta property="og:title" content="Page Not Found" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="http://developomp.com" /> <meta property="og:url" content="http://developomp.com" />
<meta <meta
property="og:image" property="og:image"
content="http://developomp.com/icon/icon.svg" content="http://developomp.com/icon/icon.svg"
/> />
<meta property="og:description" content="Page does not exist" /> <meta
</Helmet> property="og:description"
<StyledNotFound className="card main-content"> content="Page does not exist"
<Styled404>404</Styled404> />
the page you are looking for does not exist. :( </Helmet>
</StyledNotFound> <this.StyledNotFound className="card main-content">
</> <this.Styled404>404</this.Styled404>
) the page you are looking for does not exist. :(
</this.StyledNotFound>
</>
)
}
} }
export default NotFound

View file

@ -1,71 +1,83 @@
import React from "react"
import marked from "marked" import marked from "marked"
import NotFound from "./notfound" import NotFound from "./notfound"
import { Helmet } from "react-helmet-async" import { Helmet } from "react-helmet-async"
import pages from "../pages.json" import pages from "../pages.json"
import { useParams } from "react-router-dom"
function Page() { export default class Page extends React.Component {
const path = `/${useParams().path}` // eslint-disable-next-line @typescript-eslint/no-explicit-any
const fetched = pages[path] fetched: any
if (!fetched) return <NotFound />
// to prevent wrapping. I don't want to touch prettier stuff constructor(props) {
const idk = "Unknown" super(props)
fetched.content = fetched?.content ? fetched.content : "No content"
fetched.toc = fetched.meta?.toc ? fetched.meta.toc : undefined
fetched.title = fetched.meta?.title ? fetched.meta.title : "No title"
fetched.date = fetched.meta?.date ? fetched.meta.date : `${idk} date`
fetched.author = fetched.meta?.author
? fetched.meta.author
: `${idk} author`
const TableOfContents = fetched.toc && ( const fetched = pages[props.location.pathname]
<> if (!fetched) return
<div className="card">
<strong>Table of Content:</strong>
<div
className="link-color"
dangerouslySetInnerHTML={{
__html: marked(fetched.toc),
}}
></div>
</div>
<hr />
</>
) // add toc if it exists
return ( fetched.content = fetched?.content ? fetched.content : "No content"
<> fetched.toc = fetched.meta?.toc ? fetched.meta.toc : undefined
<Helmet> fetched.title = fetched.meta?.title ? fetched.meta.title : "No title"
<title>pomp | {fetched.title}</title> fetched.date = fetched.meta?.date ? fetched.meta.date : "Unknown date"
fetched.author = fetched.meta?.author
? fetched.meta.author
: "Unknown author"
<meta property="og:title" content="Page Not Found" /> this.fetched = fetched
<meta property="og:type" content="website" /> }
<meta property="og:url" content="http://developomp.com" />
<meta
property="og:image"
content="http://developomp.com/icon/icon.svg"
/>
<meta property="og:description" content="Page does not exist" />
</Helmet>
<div className="card main-content"> render() {
<h2>{fetched.title}</h2> if (!this.fetched) return <NotFound />
<small>
Published on {fetched.date} by {fetched.author} return (
</small> <>
<hr /> <Helmet>
{TableOfContents} <title>pomp | {this.fetched.title}</title>
<div
className="link-color" <meta property="og:title" content="Page Not Found" />
dangerouslySetInnerHTML={{ <meta property="og:type" content="website" />
__html: marked(fetched.content), <meta property="og:url" content="http://developomp.com" />
}} <meta
></div> property="og:image"
</div> content="http://developomp.com/icon/icon.svg"
</> />
) <meta
property="og:description"
content="Page does not exist"
/>
</Helmet>
<div className="card main-content">
<h2>{this.fetched.title}</h2>
<small>
Published on {this.fetched.date} by{" "}
{this.fetched.author}
</small>
<hr />
{
this.fetched.toc && (
<>
<div className="card">
<strong>Table of Content:</strong>
<div
className="link-color"
dangerouslySetInnerHTML={{
__html: marked(this.fetched.toc),
}}
></div>
</div>
<hr />
</>
) // add toc if it exists
}
<div
className="link-color"
dangerouslySetInnerHTML={{
__html: marked(this.fetched.content),
}}
></div>
</div>
</>
)
}
} }
export default Page

View file

@ -1,36 +1,41 @@
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"
const StyledPortfolio = styled.div`` export default class Portfolio extends React.Component {
StyledPortfolio = styled.div``
const StyledH1 = styled.h1` StyledH1 = styled.h1`
font-size: 3rem; font-size: 3rem;
` `
function Portfolio() { render() {
return ( return (
<> <>
<Helmet> <Helmet>
<title>pomp | Portfolio</title> <title>pomp | Portfolio</title>
<meta property="og:title" content="developomp's Portfolio" /> <meta
<meta property="og:type" content="website" /> property="og:title"
<meta property="og:url" content="http://developomp.com" /> content="developomp's Portfolio"
<meta />
property="og:image" <meta property="og:type" content="website" />
content="http://developomp.com/icon/icon.svg" <meta property="og:url" content="http://developomp.com" />
/> <meta
<meta property="og:image"
property="og:description" content="http://developomp.com/icon/icon.svg"
content="developomp's Portfolio" />
/> <meta
</Helmet> property="og:description"
<StyledPortfolio className="card main-content"> content="developomp's Portfolio"
<StyledH1>Portfolio</StyledH1> />
My projects </Helmet>
</StyledPortfolio> <this.StyledPortfolio className="card main-content">
</> <this.StyledH1>Portfolio</this.StyledH1>
) My projects
</this.StyledPortfolio>
</>
)
}
} }
export default Portfolio