feat: add portfolio site
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 213 KiB |
Before Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 759 KiB |
Before Width: | Height: | Size: 350 KiB |
|
@ -15,7 +15,6 @@ import Home from "./pages/Home"
|
|||
import Search from "./pages/Search"
|
||||
import Page from "./pages/Page"
|
||||
import NotFound from "./pages/NotFound"
|
||||
import Portfolio from "./pages/Portfolio"
|
||||
|
||||
import GlobalStyle from "./styles/globalStyle"
|
||||
|
||||
|
@ -83,7 +82,6 @@ export default function App() {
|
|||
<Routes>
|
||||
<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 />} />
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import dark from "@developomp-site/theme/dist/dark.json"
|
||||
import light from "@developomp-site/theme/dist/light.json"
|
||||
|
||||
import { Badge } from "@developomp-site/blog-content/src/types/types"
|
||||
import { useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
|
||||
const StyledBadge = styled.div<{ color: string; isDark: boolean }>`
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
padding: 0.2rem 0.4rem 0 0.4rem;
|
||||
margin-right: 0.4rem;
|
||||
margin-bottom: 0.4rem;
|
||||
|
||||
font-size: 0.8rem;
|
||||
|
||||
background-color: ${(props) => props.color};
|
||||
color: ${(props) =>
|
||||
props.isDark ? dark.color.text.default : light.color.text.default};
|
||||
`
|
||||
|
||||
const StyledSVG = styled.div<{ isDark: boolean }>`
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
margin-right: 0.2rem;
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
fill: ${(props) =>
|
||||
props.isDark
|
||||
? dark.color.text.default
|
||||
: light.color.text.default} !important;
|
||||
}
|
||||
`
|
||||
|
||||
interface BadgeProps {
|
||||
slug: string
|
||||
}
|
||||
|
||||
export default (props: BadgeProps) => {
|
||||
const [badgeData, setBadgeData] = useState<Badge | undefined>(undefined)
|
||||
const { slug } = props
|
||||
|
||||
const getBadgeData = async () => {
|
||||
return await require(`@developomp-site/blog-content/dist/icons/${slug}.json`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getBadgeData().then((data) => {
|
||||
setBadgeData(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!badgeData) return <></>
|
||||
|
||||
return (
|
||||
<StyledBadge color={badgeData.hex} isDark={badgeData.isDark}>
|
||||
<StyledSVG
|
||||
isDark={badgeData.isDark}
|
||||
dangerouslySetInnerHTML={{ __html: badgeData.svg }}
|
||||
/>
|
||||
<span>{badgeData.title}</span>
|
||||
</StyledBadge>
|
||||
)
|
||||
}
|
|
@ -26,7 +26,7 @@ const NavbarData: Item[] = [
|
|||
},
|
||||
{
|
||||
title: "Portfolio",
|
||||
path: "/portfolio",
|
||||
path: "https://portfolio.developomp.com",
|
||||
icon: <FontAwesomeIcon icon={faFileLines} />,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -19,10 +19,10 @@ const NotFound = () => {
|
|||
|
||||
<meta property="og:title" content="Page Not Found" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="http://developomp.com" />
|
||||
<meta property="og:url" content="http://blog.developomp.com" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="http://developomp.com/icon/icon.svg"
|
||||
content="http://blog.developomp.com/icon/icon.svg"
|
||||
/>
|
||||
<meta property="og:description" content="Page does not exist" />
|
||||
</Helmet>
|
||||
|
|
|
@ -3,12 +3,10 @@ import { Helmet } from "react-helmet-async"
|
|||
import { useLocation } from "react-router-dom"
|
||||
import styled from "styled-components"
|
||||
|
||||
import GithubLinkIcon from "../../components/GithubLinkIcon"
|
||||
import MainContent from "../../components/MainContent"
|
||||
import PostCard from "../../components/PostCard"
|
||||
import Loading from "../../components/Loading"
|
||||
import TagList from "../../components/TagList"
|
||||
import Badge from "../../components/Badge"
|
||||
import Tag from "../../components/Tag"
|
||||
import NotFound from "../NotFound"
|
||||
|
||||
|
@ -26,21 +24,10 @@ import type { PageData } from "@developomp-site/blog-content/src/types/types"
|
|||
|
||||
import contentMap from "../../contentMap"
|
||||
|
||||
const StyledTitle = styled.h1<{ pageType: PageType }>`
|
||||
const StyledTitle = styled.h1`
|
||||
margin-bottom: 1rem;
|
||||
|
||||
word-wrap: break-word;
|
||||
|
||||
${(props) => {
|
||||
if (props.pageType == PageType.PORTFOLIO_PROJECT) {
|
||||
return "margin-right: 3rem;"
|
||||
}
|
||||
}}
|
||||
`
|
||||
|
||||
const PortfolioGithubLinkContainer = styled.div`
|
||||
float: right;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const ProjectImage = styled.img`
|
||||
|
@ -97,17 +84,7 @@ export default function Page() {
|
|||
/>
|
||||
)}
|
||||
|
||||
{pageType == PageType.PORTFOLIO_PROJECT && pageData.repo && (
|
||||
<PortfolioGithubLinkContainer>
|
||||
<GithubLinkIcon link={pageData.repo} />
|
||||
</PortfolioGithubLinkContainer>
|
||||
)}
|
||||
<StyledTitle pageType={PageType.PORTFOLIO_PROJECT}>
|
||||
{pageData.title}
|
||||
</StyledTitle>
|
||||
|
||||
{pageType == PageType.PORTFOLIO_PROJECT &&
|
||||
pageData.badges.map((badge) => <Badge key={badge} slug={badge} />)}
|
||||
<StyledTitle>{pageData.title}</StyledTitle>
|
||||
|
||||
<small>
|
||||
{/* Post tags */}
|
||||
|
@ -136,10 +113,6 @@ export default function Page() {
|
|||
{/* add table of contents if it exists */}
|
||||
<Toc data={pageData.toc} />
|
||||
|
||||
{pageType == PageType.PORTFOLIO_PROJECT && (
|
||||
<ProjectImage src={pageData.image} alt="project example image" />
|
||||
)}
|
||||
|
||||
{/* page content */}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
|
||||
import MainContent from "../../components/MainContent"
|
||||
import Badge from "../../components/Badge"
|
||||
import ProjectCard from "./ProjectCard"
|
||||
|
||||
import portfolio from "@developomp-site/blog-content/dist/portfolio.json"
|
||||
|
||||
import type { PortfolioProject } from "@developomp-site/blog-content/src/types/types"
|
||||
|
||||
const Portfolio = () => {
|
||||
const [projects, setProjects] = useState<JSX.Element[]>([])
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [skills, setSkills] = useState<JSX.Element[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const _projects: JSX.Element[] = []
|
||||
|
||||
for (const projectID in portfolio.projects) {
|
||||
_projects.push(
|
||||
<ProjectCard
|
||||
key={projectID}
|
||||
projectID={projectID}
|
||||
project={
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
portfolio.projects[projectID] as PortfolioProject
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
setProjects(_projects)
|
||||
|
||||
setSkills(
|
||||
portfolio.skills.map((slug) => {
|
||||
return <Badge key={slug} slug={slug} />
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>pomp | Portfolio</title>
|
||||
|
||||
<meta property="og:title" content="Portfolio" />
|
||||
<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="developomp's Portfolio" />
|
||||
</Helmet>
|
||||
|
||||
<MainContent>
|
||||
<h1>Portfolio</h1>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Projects */}
|
||||
|
||||
<h2 id="projects">
|
||||
<a className="header-anchor" href="#projects">
|
||||
#
|
||||
</a>
|
||||
{" Projects"}
|
||||
</h2>
|
||||
|
||||
{/* todo: filter projects by skill */}
|
||||
|
||||
{skills}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{projects}
|
||||
</MainContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Portfolio
|
|
@ -1,64 +0,0 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import Badge from "../../components/Badge"
|
||||
import { cardCSS } from "../../components/Card"
|
||||
|
||||
import { PortfolioProject } from "@developomp-site/blog-content/src/types/types"
|
||||
|
||||
const StyledProjectCard = styled.div`
|
||||
${cardCSS}
|
||||
|
||||
color: ${(props) => props.theme.theme.color.text.default};
|
||||
margin-bottom: 2rem;
|
||||
word-wrap: break-word;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
|
||||
box-shadow: 0 4px 10px
|
||||
${(props) => props.theme.theme.component.card.color.hoverGlow};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledImg = styled.img`
|
||||
width: 100%;
|
||||
|
||||
object-fit: cover;
|
||||
margin-bottom: 1rem;
|
||||
`
|
||||
|
||||
interface ProjectCardProps {
|
||||
projectID: string
|
||||
project: PortfolioProject
|
||||
}
|
||||
|
||||
const ProjectCard = (props: ProjectCardProps) => {
|
||||
const { projectID, project } = props
|
||||
|
||||
const [badges, setBadges] = useState<JSX.Element[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setBadges(project.badges.map((badge) => <Badge key={badge} slug={badge} />))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Link to={projectID}>
|
||||
<StyledProjectCard>
|
||||
<h1>{project.name}</h1>
|
||||
<StyledImg src={project.image} />
|
||||
|
||||
{badges}
|
||||
<hr />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: project.overview,
|
||||
}}
|
||||
/>
|
||||
</StyledProjectCard>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectCard
|
|
@ -1,3 +0,0 @@
|
|||
import Portfolio from "./Portfolio"
|
||||
|
||||
export default Portfolio
|
30
apps/portfolio/.eslintrc
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react-refresh",
|
||||
"prettier",
|
||||
"simple-import-sort"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": "warn",
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
}
|
||||
}
|
24
apps/portfolio/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
5
apps/portfolio/.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
23
apps/portfolio/index.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta name="description" content="developomp's portfolio" />
|
||||
<title>portfolio</title>
|
||||
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:title" content="Portfolio" />
|
||||
<meta property="og:site_name" content="pomp's portfolio" />
|
||||
<meta property="og:description" content="developomp's Portfolio" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="http://portfolio.developomp.com" />
|
||||
<meta property="og:image" content="/favicon.svg" />
|
||||
</head>
|
||||
<body class="dark">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
49
apps/portfolio/package.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@developomp-site/portfolio",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans-kr": "^5.0.3",
|
||||
"@fontsource/source-code-pro": "^5.0.3",
|
||||
"@linaria/core": "^4.2.10",
|
||||
"@linaria/react": "^4.3.8",
|
||||
"hoofd": "^1.7.0",
|
||||
"react": "^18.2.0",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"wouter": "^2.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@developomp-site/blog-content": "workspace:*",
|
||||
"@linaria/babel-preset": "^4.4.5",
|
||||
"@linaria/vite": "^4.2.11",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.1.5",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-dynamic-import": "^1.4.1",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
}
|
||||
}
|
6
apps/portfolio/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
1
apps/portfolio/public/favicon.svg
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
apps/portfolio/public/img/portfolio/developomp.com.avif
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
apps/portfolio/public/img/portfolio/exyleio.avif
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/portfolio/public/img/portfolio/linux-setup-script.avif
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
apps/portfolio/public/img/portfolio/llama-bot.avif
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
apps/portfolio/public/img/portfolio/mocha-downloader.avif
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/portfolio/public/img/portfolio/pomky.avif
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
apps/portfolio/public/img/portfolio/wbm-installer.avif
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
apps/portfolio/public/img/portfolio/wbm-overlays.avif
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
apps/portfolio/public/img/portfolio/wbm.avif
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
apps/portfolio/public/img/portfolio/wbtimeline.avif
Normal file
After Width: | Height: | Size: 83 KiB |
35
apps/portfolio/src/App.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useTitleTemplate } from "hoofd"
|
||||
import { type FC } from "react"
|
||||
import { Route, Switch } from "wouter"
|
||||
|
||||
import Header from "./components/Header"
|
||||
import Home from "./routes/Home"
|
||||
import NotFound from "./routes/NotFound"
|
||||
import Project from "./routes/Project"
|
||||
|
||||
const App: FC = () => {
|
||||
useTitleTemplate("Portfolio | %s")
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="mb-10 mt-20 w-full max-w-screen-md px-4">
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
|
||||
<Route path="/project/:id">
|
||||
<Project />
|
||||
</Route>
|
||||
|
||||
<Route>
|
||||
<NotFound />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
45
apps/portfolio/src/components/Badge/Badge.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import "./style.scss"
|
||||
|
||||
import { type Badge as BadgeType } from "@developomp-site/blog-content/src/types/types"
|
||||
import { type FC, useEffect, useState } from "react"
|
||||
|
||||
interface BadgeProps {
|
||||
slug: string
|
||||
}
|
||||
|
||||
const Badge: FC<BadgeProps> = ({ slug }) => {
|
||||
const [badgeData, setBadgeData] = useState<BadgeType | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
setBadgeData(
|
||||
await import(
|
||||
`@developomp-site/blog-content/dist/icons/${slug}.json`
|
||||
)
|
||||
)
|
||||
})()
|
||||
}, [slug])
|
||||
|
||||
if (!badgeData) return <></>
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: badgeData.hex }}
|
||||
className={`mb-2 mr-2 flex w-fit items-center px-2 py-1 text-xs ${
|
||||
badgeData.isDark
|
||||
? "text-dark-text-default"
|
||||
: "text-light-text-default"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
badgeData.isDark ? "dark-badge" : "light-badge"
|
||||
} badge mr-1 inline-block w-6 align-middle`}
|
||||
dangerouslySetInnerHTML={{ __html: badgeData.svg }}
|
||||
/>
|
||||
<span>{badgeData.title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Badge
|
3
apps/portfolio/src/components/Badge/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Badge from "./Badge"
|
||||
|
||||
export default Badge
|
11
apps/portfolio/src/components/Badge/style.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.light-badge {
|
||||
svg {
|
||||
@apply dark:fill-light-text-default;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-badge {
|
||||
svg {
|
||||
@apply dark:fill-dark-text-default;
|
||||
}
|
||||
}
|
24
apps/portfolio/src/components/Header/Header.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { type FC } from "react"
|
||||
import { Link } from "wouter"
|
||||
|
||||
const Header: FC = () => {
|
||||
return (
|
||||
<header className="fixed top-0 z-50 flex w-screen justify-center dark:bg-dark-ui dark:text-dark-text-default">
|
||||
<div className="my-0 flex h-16 w-full max-w-5xl items-center">
|
||||
<Link
|
||||
className="flex items-center"
|
||||
to="/"
|
||||
aria-label="homepage"
|
||||
>
|
||||
<img
|
||||
className="m-4 block h-10 cursor-pointer"
|
||||
src="/favicon.svg"
|
||||
alt="logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
3
apps/portfolio/src/components/Header/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Header from "./Header"
|
||||
|
||||
export default Header
|
143
apps/portfolio/src/components/Loading.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* inspired by https://codepen.io/avstorm/pen/RwNzPNN
|
||||
*/
|
||||
|
||||
// import styled from "styled-components"
|
||||
|
||||
// import MainContent from "./MainContent"
|
||||
|
||||
// const StyledContainer = styled(MainContent)`
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// text-align: center;
|
||||
|
||||
// animation: fadein 2s;
|
||||
// @keyframes fadein {
|
||||
// from {
|
||||
// opacity: 0;
|
||||
// }
|
||||
// 50% {
|
||||
// opacity: 0;
|
||||
// }
|
||||
// to {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
// const StyledSVG = styled.svg`
|
||||
// --color: ${({ theme }) => theme.theme.color.text.default};
|
||||
|
||||
// display: block;
|
||||
// margin: 1rem;
|
||||
// margin-bottom: 4.5rem;
|
||||
|
||||
// transform: scale(2);
|
||||
|
||||
// #teabag {
|
||||
// transform-origin: top center;
|
||||
// transform: rotate(3deg);
|
||||
// animation: swingAnimation 2s infinite;
|
||||
// }
|
||||
|
||||
// #steamL {
|
||||
// stroke-dasharray: 13;
|
||||
// stroke-dashoffset: 13;
|
||||
// animation: steamLargeAnimation 2s infinite;
|
||||
// }
|
||||
|
||||
// #steamR {
|
||||
// stroke-dasharray: 9;
|
||||
// stroke-dashoffset: 9;
|
||||
// animation: steamSmallAnimation 2s infinite;
|
||||
// }
|
||||
|
||||
// @keyframes swingAnimation {
|
||||
// 50% {
|
||||
// transform: rotate(-3deg);
|
||||
// }
|
||||
// }
|
||||
|
||||
// @keyframes steamLargeAnimation {
|
||||
// 0% {
|
||||
// stroke-dashoffset: 13;
|
||||
// opacity: 0.6;
|
||||
// }
|
||||
// 100% {
|
||||
// stroke-dashoffset: 39;
|
||||
// opacity: 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @keyframes steamSmallAnimation {
|
||||
// 10% {
|
||||
// stroke-dashoffset: 9;
|
||||
// opacity: 0.6;
|
||||
// }
|
||||
// 80% {
|
||||
// stroke-dashoffset: 27;
|
||||
// opacity: 0;
|
||||
// }
|
||||
// 100% {
|
||||
// stroke-dashoffset: 27;
|
||||
// opacity: 0;
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
|
||||
// const Loading = () => {
|
||||
// return (
|
||||
// <StyledContainer>
|
||||
// <StyledSVG
|
||||
// width="37"
|
||||
// height="48"
|
||||
// viewBox="0 0 37 48"
|
||||
// fill="none"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <path
|
||||
// d="M27.0819 17H3.02508C1.91076 17 1.01376 17.9059 1.0485 19.0197C1.15761 22.5177 1.49703 29.7374 2.5 34C4.07125 40.6778 7.18553 44.8868 8.44856 46.3845C8.79051 46.79 9.29799 47 9.82843 47H20.0218C20.639 47 21.2193 46.7159 21.5659 46.2052C22.6765 44.5687 25.2312 40.4282 27.5 34C28.9757 29.8188 29.084 22.4043 29.0441 18.9156C29.0319 17.8436 28.1539 17 27.0819 17Z"
|
||||
// stroke="var(--color)"
|
||||
// strokeWidth="2"
|
||||
// />
|
||||
// <path
|
||||
// d="M29 23.5C29 23.5 34.5 20.5 35.5 25.4999C36.0986 28.4926 34.2033 31.5383 32 32.8713C29.4555 34.4108 28 34 28 34"
|
||||
// stroke="var(--color)"
|
||||
// strokeWidth="2"
|
||||
// />
|
||||
// <path
|
||||
// id="teabag"
|
||||
// fill="var(--color)"
|
||||
// fillRule="evenodd"
|
||||
// clipRule="evenodd"
|
||||
// d="M16 25V17H14V25H12C10.3431 25 9 26.3431 9 28V34C9 35.6569 10.3431 37 12 37H18C19.6569 37 21 35.6569 21 34V28C21 26.3431 19.6569 25 18 25H16ZM11 28C11 27.4477 11.4477 27 12 27H18C18.5523 27 19 27.4477 19 28V34C19 34.5523 18.5523 35 18 35H12C11.4477 35 11 34.5523 11 34V28Z"
|
||||
// />
|
||||
// <path
|
||||
// id="steamL"
|
||||
// d="M17 1C17 1 17 4.5 14 6.5C11 8.5 11 12 11 12"
|
||||
// strokeWidth="2"
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// stroke="var(--color)"
|
||||
// />
|
||||
// <path
|
||||
// id="steamR"
|
||||
// d="M21 6C21 6 21 8.22727 19 9.5C17 10.7727 17 13 17 13"
|
||||
// stroke="var(--color)"
|
||||
// strokeWidth="2"
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// />
|
||||
// </StyledSVG>
|
||||
// <h2>Loading...</h2>
|
||||
// </StyledContainer>
|
||||
// )
|
||||
// }
|
||||
|
||||
const Loading = () => {
|
||||
return <>Loading</>
|
||||
}
|
||||
|
||||
export default Loading
|
44
apps/portfolio/src/components/ProjectCard/ProjectCard.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import "./style.scss"
|
||||
|
||||
import { PortfolioProject } from "@developomp-site/blog-content/src/types/types"
|
||||
import { type FC, useEffect, useState } from "react"
|
||||
|
||||
import Badge from "@/components/Badge"
|
||||
|
||||
interface ProjectCardProps {
|
||||
projectID: string
|
||||
project: PortfolioProject
|
||||
}
|
||||
|
||||
const ProjectCard: FC<ProjectCardProps> = ({ projectID, project }) => {
|
||||
const [badges, setBadges] = useState<JSX.Element[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setBadges(
|
||||
project.badges.map((badge) => <Badge key={badge} slug={badge} />)
|
||||
)
|
||||
}, [project.badges])
|
||||
|
||||
return (
|
||||
<a href={`/project/${projectID}`}>
|
||||
<div className="project">
|
||||
<h2>{project.name}</h2>
|
||||
<img
|
||||
className="mb-4 w-full object-cover"
|
||||
src={project.image}
|
||||
alt="project thumbnail"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap">{badges}</div>
|
||||
<hr className="my-1" />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: project.overview,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectCard
|
3
apps/portfolio/src/components/ProjectCard/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import ProjectCard from "./ProjectCard"
|
||||
|
||||
export default ProjectCard
|
17
apps/portfolio/src/components/ProjectCard/style.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
.project {
|
||||
// general
|
||||
@apply cursor-pointer rounded-md;
|
||||
|
||||
// spacing
|
||||
@apply m-auto mb-8 p-8;
|
||||
|
||||
// color
|
||||
@apply bg-dark-card-bg dark:text-dark-text-default;
|
||||
|
||||
// glow
|
||||
@apply duration-75 hover:shadow-glow dark:hover:shadow-dark-text-default;
|
||||
|
||||
h2 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
}
|
75
apps/portfolio/src/components/Toc/Toc.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import "./style.scss"
|
||||
|
||||
import { styled } from "@linaria/react"
|
||||
import { type FC, useState } from "react"
|
||||
import { Collapse } from "react-collapse"
|
||||
|
||||
const StyledTocToggleButton = styled.button`
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-align: left;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
`
|
||||
|
||||
const StyledCollapseContainer = styled.div`
|
||||
* {
|
||||
transition: height 200ms ease-out;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: circle;
|
||||
padding-left: 2.5rem;
|
||||
list-style-position: inside;
|
||||
}
|
||||
`
|
||||
|
||||
const Toc: FC<{ data?: string }> = (props) => {
|
||||
const [isTocOpened, setIsTocOpened] = useState(false)
|
||||
|
||||
if (!props.data) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTocToggleButton
|
||||
className="text-light-text-high-contrast dark:text-dark-text-high-contrast"
|
||||
onClick={() => {
|
||||
setIsTocOpened((prev) => !prev)
|
||||
}}
|
||||
>
|
||||
<strong className="flex items-center justify-center gap-1 fill-light-text-high-contrast dark:fill-dark-text-high-contrast">
|
||||
Table of Contents
|
||||
{isTocOpened ? (
|
||||
// Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="1em"
|
||||
viewBox="0 0 320 512"
|
||||
>
|
||||
<path d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z" />
|
||||
</svg>
|
||||
) : (
|
||||
// Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="1em"
|
||||
viewBox="0 0 320 512"
|
||||
>
|
||||
<path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z" />
|
||||
</svg>
|
||||
)}
|
||||
</strong>
|
||||
</StyledTocToggleButton>
|
||||
<StyledCollapseContainer>
|
||||
<Collapse isOpened={isTocOpened}>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.data }} />
|
||||
</Collapse>
|
||||
</StyledCollapseContainer>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toc
|
3
apps/portfolio/src/components/Toc/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Toc from "./Toc"
|
||||
|
||||
export default Toc
|
0
apps/portfolio/src/components/Toc/style.scss
Normal file
40
apps/portfolio/src/index.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
@apply scroll-m-16;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-dark-ui-bg font-noto-sans text-dark-text-default;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply flex min-h-screen w-full flex-col items-center;
|
||||
}
|
||||
|
||||
#root {
|
||||
@apply m-0 flex h-full w-full scroll-m-16 flex-col items-center p-0;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-anchor visited:text-anchor hover:text-anchor-accent active:text-anchor-accent;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply w-fit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-bold;
|
||||
|
||||
a {
|
||||
@apply dark:text-dark-anchor-header;
|
||||
}
|
||||
}
|
12
apps/portfolio/src/main.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import "@fontsource/noto-sans-kr/400.css"
|
||||
import "@fontsource/noto-sans-kr/700.css"
|
||||
import "@fontsource/source-code-pro"
|
||||
import "./index.scss"
|
||||
|
||||
import ReactDOM from "react-dom/client"
|
||||
|
||||
import App from "./App.tsx"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<App />
|
||||
)
|
43
apps/portfolio/src/routes/Home/Home.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import "./style.scss"
|
||||
|
||||
import portfolio from "@developomp-site/blog-content/dist/portfolio.json"
|
||||
import type { PortfolioProject } from "@developomp-site/blog-content/src/types/types"
|
||||
import { useTitle } from "hoofd"
|
||||
import { type FC } from "react"
|
||||
|
||||
import Badge from "@/components/Badge"
|
||||
import ProjectCard from "@/components/ProjectCard"
|
||||
|
||||
const projects: JSX.Element[] = []
|
||||
const skills: JSX.Element[] = portfolio.skills.map((slug) => {
|
||||
return <Badge key={slug} slug={slug} />
|
||||
})
|
||||
|
||||
for (const projectID in portfolio.projects) {
|
||||
projects.push(
|
||||
<ProjectCard
|
||||
key={projectID}
|
||||
projectID={projectID}
|
||||
project={
|
||||
portfolio.projects[
|
||||
projectID as keyof typeof portfolio.projects
|
||||
] as PortfolioProject
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Home: FC = () => {
|
||||
useTitle("Home")
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-8 text-5xl">developomp's Portfolio</h1>
|
||||
<hr />
|
||||
<div className="my-4 flex flex-wrap">{skills}</div>
|
||||
<div className="projects">{projects}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
3
apps/portfolio/src/routes/Home/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Home from "./Home"
|
||||
|
||||
export default Home
|
5
apps/portfolio/src/routes/Home/style.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.projects {
|
||||
h2 {
|
||||
@apply mb-4;
|
||||
}
|
||||
}
|
22
apps/portfolio/src/routes/NotFound/NotFound.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import "./style.css"
|
||||
|
||||
import { type FC } from "react"
|
||||
|
||||
const NotFound: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<h1 className="w-fit px-4 py-2 text-7xl dark:bg-dark-text-default dark:text-dark-ui-bg">
|
||||
404
|
||||
</h1>
|
||||
|
||||
<h2
|
||||
className="glitch layers text-8xl"
|
||||
data-text="404 ERROR 404 ERROR"
|
||||
>
|
||||
Page Not Found
|
||||
</h2>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
3
apps/portfolio/src/routes/NotFound/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import NotFound from "./NotFound"
|
||||
|
||||
export default NotFound
|
389
apps/portfolio/src/routes/NotFound/style.css
Normal file
|
@ -0,0 +1,389 @@
|
|||
/* glitch */
|
||||
|
||||
.glitch span {
|
||||
animation: paths 5s step-end infinite;
|
||||
}
|
||||
|
||||
.glitch::before {
|
||||
animation: paths 5s step-end infinite, opacity 5s step-end infinite,
|
||||
font 8s step-end infinite, movement 10s step-end infinite;
|
||||
}
|
||||
|
||||
.glitch::after {
|
||||
animation: paths 5s step-end infinite, opacity 5s step-end infinite,
|
||||
font 7s step-end infinite, movement 8s step-end infinite;
|
||||
}
|
||||
|
||||
/* layers */
|
||||
|
||||
.layers {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layers::before,
|
||||
.layers::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
width: 110%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.layers::before {
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
color: #e0287d;
|
||||
}
|
||||
|
||||
.layers::after {
|
||||
top: 5px;
|
||||
left: -10px;
|
||||
color: #1bc7fb;
|
||||
}
|
||||
|
||||
/* keyframes */
|
||||
|
||||
@keyframes paths {
|
||||
0% {
|
||||
clip-path: polygon(
|
||||
0% 43%,
|
||||
83% 43%,
|
||||
83% 22%,
|
||||
23% 22%,
|
||||
23% 24%,
|
||||
91% 24%,
|
||||
91% 26%,
|
||||
18% 26%,
|
||||
18% 83%,
|
||||
29% 83%,
|
||||
29% 17%,
|
||||
41% 17%,
|
||||
41% 39%,
|
||||
18% 39%,
|
||||
18% 82%,
|
||||
54% 82%,
|
||||
54% 88%,
|
||||
19% 88%,
|
||||
19% 4%,
|
||||
39% 4%,
|
||||
39% 14%,
|
||||
76% 14%,
|
||||
76% 52%,
|
||||
23% 52%,
|
||||
23% 35%,
|
||||
19% 35%,
|
||||
19% 8%,
|
||||
36% 8%,
|
||||
36% 31%,
|
||||
73% 31%,
|
||||
73% 16%,
|
||||
1% 16%,
|
||||
1% 56%,
|
||||
50% 56%,
|
||||
50% 8%
|
||||
);
|
||||
}
|
||||
|
||||
5% {
|
||||
clip-path: polygon(
|
||||
0% 29%,
|
||||
44% 29%,
|
||||
44% 83%,
|
||||
94% 83%,
|
||||
94% 56%,
|
||||
11% 56%,
|
||||
11% 64%,
|
||||
94% 64%,
|
||||
94% 70%,
|
||||
88% 70%,
|
||||
88% 32%,
|
||||
18% 32%,
|
||||
18% 96%,
|
||||
10% 96%,
|
||||
10% 62%,
|
||||
9% 62%,
|
||||
9% 84%,
|
||||
68% 84%,
|
||||
68% 50%,
|
||||
52% 50%,
|
||||
52% 55%,
|
||||
35% 55%,
|
||||
35% 87%,
|
||||
25% 87%,
|
||||
25% 39%,
|
||||
15% 39%,
|
||||
15% 88%,
|
||||
52% 88%
|
||||
);
|
||||
}
|
||||
|
||||
30% {
|
||||
clip-path: polygon(
|
||||
0% 53%,
|
||||
93% 53%,
|
||||
93% 62%,
|
||||
68% 62%,
|
||||
68% 37%,
|
||||
97% 37%,
|
||||
97% 89%,
|
||||
13% 89%,
|
||||
13% 45%,
|
||||
51% 45%,
|
||||
51% 88%,
|
||||
17% 88%,
|
||||
17% 54%,
|
||||
81% 54%,
|
||||
81% 75%,
|
||||
79% 75%,
|
||||
79% 76%,
|
||||
38% 76%,
|
||||
38% 28%,
|
||||
61% 28%,
|
||||
61% 12%,
|
||||
55% 12%,
|
||||
55% 62%,
|
||||
68% 62%,
|
||||
68% 51%,
|
||||
0% 51%,
|
||||
0% 92%,
|
||||
63% 92%,
|
||||
63% 4%,
|
||||
65% 4%
|
||||
);
|
||||
}
|
||||
|
||||
45% {
|
||||
clip-path: polygon(
|
||||
0% 33%,
|
||||
2% 33%,
|
||||
2% 69%,
|
||||
58% 69%,
|
||||
58% 94%,
|
||||
55% 94%,
|
||||
55% 25%,
|
||||
33% 25%,
|
||||
33% 85%,
|
||||
16% 85%,
|
||||
16% 19%,
|
||||
5% 19%,
|
||||
5% 20%,
|
||||
79% 20%,
|
||||
79% 96%,
|
||||
93% 96%,
|
||||
93% 50%,
|
||||
5% 50%,
|
||||
5% 74%,
|
||||
55% 74%,
|
||||
55% 57%,
|
||||
96% 57%,
|
||||
96% 59%,
|
||||
87% 59%,
|
||||
87% 65%,
|
||||
82% 65%,
|
||||
82% 39%,
|
||||
63% 39%,
|
||||
63% 92%,
|
||||
4% 92%,
|
||||
4% 36%,
|
||||
24% 36%,
|
||||
24% 70%,
|
||||
1% 70%,
|
||||
1% 43%,
|
||||
15% 43%,
|
||||
15% 28%,
|
||||
23% 28%,
|
||||
23% 71%,
|
||||
90% 71%,
|
||||
90% 86%,
|
||||
97% 86%,
|
||||
97% 1%,
|
||||
60% 1%,
|
||||
60% 67%,
|
||||
71% 67%,
|
||||
71% 91%,
|
||||
17% 91%,
|
||||
17% 14%,
|
||||
39% 14%,
|
||||
39% 30%,
|
||||
58% 30%,
|
||||
58% 11%,
|
||||
52% 11%,
|
||||
52% 83%,
|
||||
68% 83%
|
||||
);
|
||||
}
|
||||
|
||||
76% {
|
||||
clip-path: polygon(
|
||||
0% 26%,
|
||||
15% 26%,
|
||||
15% 73%,
|
||||
72% 73%,
|
||||
72% 70%,
|
||||
77% 70%,
|
||||
77% 75%,
|
||||
8% 75%,
|
||||
8% 42%,
|
||||
4% 42%,
|
||||
4% 61%,
|
||||
17% 61%,
|
||||
17% 12%,
|
||||
26% 12%,
|
||||
26% 63%,
|
||||
73% 63%,
|
||||
73% 43%,
|
||||
90% 43%,
|
||||
90% 67%,
|
||||
50% 67%,
|
||||
50% 41%,
|
||||
42% 41%,
|
||||
42% 46%,
|
||||
50% 46%,
|
||||
50% 84%,
|
||||
96% 84%,
|
||||
96% 78%,
|
||||
49% 78%,
|
||||
49% 25%,
|
||||
63% 25%,
|
||||
63% 14%
|
||||
);
|
||||
}
|
||||
|
||||
90% {
|
||||
clip-path: polygon(
|
||||
0% 41%,
|
||||
13% 41%,
|
||||
13% 6%,
|
||||
87% 6%,
|
||||
87% 93%,
|
||||
10% 93%,
|
||||
10% 13%,
|
||||
89% 13%,
|
||||
89% 6%,
|
||||
3% 6%,
|
||||
3% 8%,
|
||||
16% 8%,
|
||||
16% 79%,
|
||||
0% 79%,
|
||||
0% 99%,
|
||||
92% 99%,
|
||||
92% 90%,
|
||||
5% 90%,
|
||||
5% 60%,
|
||||
0% 60%,
|
||||
0% 48%,
|
||||
89% 48%,
|
||||
89% 13%,
|
||||
80% 13%,
|
||||
80% 43%,
|
||||
95% 43%,
|
||||
95% 19%,
|
||||
80% 19%,
|
||||
80% 85%,
|
||||
38% 85%,
|
||||
38% 62%
|
||||
);
|
||||
}
|
||||
|
||||
1%,
|
||||
7%,
|
||||
33%,
|
||||
47%,
|
||||
78%,
|
||||
93% {
|
||||
clip-path: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes movement {
|
||||
0% {
|
||||
top: 0px;
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
15% {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
60% {
|
||||
top: 5px;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
75% {
|
||||
top: -5px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 10px;
|
||||
left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes opacity {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
5% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
45% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
1%,
|
||||
7%,
|
||||
33%,
|
||||
47%,
|
||||
78%,
|
||||
93% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes font {
|
||||
0% {
|
||||
font-weight: 100;
|
||||
color: #e0287d;
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
20% {
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
font-weight: 300;
|
||||
color: #1bc7fb;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
60% {
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
90% {
|
||||
font-weight: 500;
|
||||
color: #e0287d;
|
||||
filter: blur(6px);
|
||||
}
|
||||
}
|
92
apps/portfolio/src/routes/Project/Project.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import "./style.scss"
|
||||
|
||||
import portfolio from "@developomp-site/blog-content/dist/portfolio.json"
|
||||
import { useMeta, useTitle } from "hoofd"
|
||||
import { type FC, useEffect, useState } from "react"
|
||||
import { useRoute } from "wouter"
|
||||
|
||||
import Badge from "@/components/Badge"
|
||||
import Loading from "@/components/Loading"
|
||||
import Toc from "@/components/Toc"
|
||||
import NotFound from "@/routes/NotFound"
|
||||
|
||||
export interface PageData {
|
||||
title: string
|
||||
toc?: string
|
||||
content: string
|
||||
|
||||
image: string // image url
|
||||
overview: string
|
||||
badges: string[]
|
||||
repo: string
|
||||
}
|
||||
|
||||
const Project: FC = () => {
|
||||
const [pageData, setPageData] = useState<PageData | undefined>(undefined)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const [match, params] = useRoute("/project/:id")
|
||||
|
||||
useTitle(pageData?.title || "Loading")
|
||||
useMeta({ property: "og:title", content: pageData?.title })
|
||||
|
||||
useEffect(() => {
|
||||
if (!match) return
|
||||
;(async () => {
|
||||
try {
|
||||
if (!(params.id in portfolio.projects)) return
|
||||
|
||||
const data =
|
||||
portfolio.projects[
|
||||
params.id as keyof typeof portfolio.projects
|
||||
]
|
||||
|
||||
const fetched_content = await import(
|
||||
`@developomp-site/blog-content/dist/content/projects/${params.id}.json`
|
||||
)
|
||||
|
||||
setPageData({
|
||||
content: fetched_content.content,
|
||||
toc: fetched_content.toc,
|
||||
title: data.name,
|
||||
image: data.image,
|
||||
overview: data.overview,
|
||||
badges: data.badges,
|
||||
repo: data.repo,
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
})()
|
||||
}, [match, params])
|
||||
|
||||
if (!match) return <NotFound />
|
||||
if (isLoading) return <Loading />
|
||||
if (!pageData) return <NotFound />
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-4 text-4xl">{pageData.title}</h1>
|
||||
<div className="flex flex-wrap">
|
||||
{pageData.badges.map((slug) => {
|
||||
return <Badge key={slug} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<Toc data={pageData.toc} />
|
||||
|
||||
{/* page content */}
|
||||
<div
|
||||
className="project-description"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageData.content,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Project
|
3
apps/portfolio/src/routes/Project/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Project from "./Project"
|
||||
|
||||
export default Project
|
29
apps/portfolio/src/routes/Project/style.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.project-description {
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply mt-10 text-3xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply mt-6 indent-2 text-xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply mt-6 indent-4 text-base;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply mt-6 indent-6 text-base;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply mt-6 indent-8 text-base;
|
||||
}
|
||||
}
|
47
apps/portfolio/src/themeToggle.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
type Theme = "dark" | "light" | "system"
|
||||
|
||||
/**
|
||||
* Reads theme from local storage
|
||||
*/
|
||||
export function readTheme(): Theme {
|
||||
const data = localStorage.getItem("theme")
|
||||
|
||||
if (
|
||||
!data || // data is falsy
|
||||
(data && data != "dark" && data != "light" && data != "system") // data is a non-empty string that's not a valid Theme
|
||||
) {
|
||||
saveTheme("system")
|
||||
return "system"
|
||||
}
|
||||
|
||||
return data as Theme
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves and sets the theme of the site at the same time
|
||||
*/
|
||||
export function saveTheme(theme: Theme): void {
|
||||
localStorage.setItem("theme", theme)
|
||||
setTheme(theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme of the site without saving it
|
||||
*/
|
||||
export function setTheme(theme: Theme): void {
|
||||
if (theme === "dark") document.documentElement.classList.add("dark")
|
||||
else document.documentElement.classList.remove("dark")
|
||||
}
|
||||
|
||||
// watch theme preference state
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", ({ matches }) => {
|
||||
// only respond to the event if the theme is set to system
|
||||
if (readTheme() != "system") return
|
||||
|
||||
document.documentElement.classList.add("dark")
|
||||
document.documentElement.classList.remove("dark")
|
||||
|
||||
setTheme(matches ? "dark" : "light")
|
||||
})
|
1
apps/portfolio/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
134
apps/portfolio/tailwind.config.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: "class",
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// UI
|
||||
/***/ "dark-ui": "#202225",
|
||||
/**/ "light-ui": "#FFFFFF",
|
||||
/***/ "dark-ui-hover": "#3F3F46",
|
||||
/**/ "light-ui-hover": "#EEEEEE",
|
||||
/***/ "dark-ui-bg": "#36393F",
|
||||
/**/ "light-ui-bg": "#F7F7F7",
|
||||
/***/ "dark-ui-border": "#555",
|
||||
/**/ "light-ui-border": "#CCC",
|
||||
|
||||
// text
|
||||
/***/ "dark-text-default": "#EEEEEE",
|
||||
/**/ "light-text-default": "#111111",
|
||||
/***/ "dark-text-high-contrast": "#FFFFFF",
|
||||
/**/ "light-text-high-contrast": "#000000",
|
||||
/***/ "dark-text-gray": "#CCC",
|
||||
/**/ "light-text-gray": "#555",
|
||||
|
||||
// anchor
|
||||
/*********/ anchor: "#66AAFF",
|
||||
/********/ "anchor-accent": "#4592F7",
|
||||
/***/ "dark-anchor-header": "#778899",
|
||||
/**/ "light-anchor-header": "#D3D3D3",
|
||||
|
||||
// card
|
||||
/***/ "dark-card-bg": "#2F3136",
|
||||
/**/ "light-card-bg": "#FFFFFF",
|
||||
/***/ "dark-card-glow": "#FFFFFF33",
|
||||
/**/ "light-card-glow": "#00000040",
|
||||
|
||||
// blockquote
|
||||
/***/ "dark-blockquote-bg": "#FFFFFF12",
|
||||
/**/ "light-blockquote-bg": "#0000000D",
|
||||
/***/ "dark-blockquote-accent": "#FFFFFF4D",
|
||||
/**/ "light-blockquote-accent": "#0000001A",
|
||||
|
||||
// inline code
|
||||
/***/ "dark-inline-code-bg": "#444",
|
||||
/**/ "light-inline-code-bg": "#EEE",
|
||||
/***/ "dark-inline-code-text": "#FFFFFF",
|
||||
/**/ "light-inline-code-text": "#000000",
|
||||
/***/ "dark-inline-code-border": "#666",
|
||||
/**/ "light-inline-code-border": "#BBB",
|
||||
|
||||
// block code
|
||||
// light theme using: highlight.js/styles/default.css
|
||||
// dark theme using: highlight.js/styles/atom-one-dark-reasonable.css
|
||||
/***/ "dark-block-code-border": "#555",
|
||||
/**/ "light-block-code-border": "#BBB",
|
||||
/***/ "dark-block-code-highlight": "#14161A",
|
||||
/**/ "light-block-code-highlight": "#DDDDDD",
|
||||
|
||||
// footer
|
||||
/***/ "dark-footer-bg": "#000000",
|
||||
/**/ "light-footer-bg": "#FFFFFF",
|
||||
/***/ "dark-footer-text": "#808080",
|
||||
/* */ "light-footer-text": "#808080",
|
||||
|
||||
// header
|
||||
/***/ "dark-header-bg": "#202225",
|
||||
/**/ "light-header-bg": "",
|
||||
/***/ "dark-header-hover": "#3F3F46",
|
||||
/**/ "light-header-hover": "",
|
||||
/***/ "dark-header-text": "#D4D4D8",
|
||||
/**/ "light-header-text": "",
|
||||
|
||||
// input
|
||||
/***/ "dark-input-bg": "#36393F",
|
||||
/**/ "light-input-bg": "#EEEEEE",
|
||||
/***/ "dark-input-item-hover": "#202225",
|
||||
/**/ "light-input-item-hover": "#FFFFFF",
|
||||
/***/ "dark-input-border": "#555555",
|
||||
/**/ "light-input-border": "#CCCCCC",
|
||||
/***/ "dark-input-border-hover": "#808080",
|
||||
/**/ "light-input-border-hover": "#808080",
|
||||
/***/ "dark-input-border-focus": "#A3A3A3",
|
||||
/**/ "light-input-border-focus": "#000000",
|
||||
/***/ "dark-input-placeholder": "#A9A9A9",
|
||||
/**/ "light-input-placeholder": "#777777",
|
||||
|
||||
// kbd
|
||||
/***/ "dark-kbd-bg": "#000000",
|
||||
/**/ "light-kbd-bg": "#F7F7F7",
|
||||
/***/ "dark-kbd-text": "#FFFFFF",
|
||||
/**/ "light-kbd-text": "#333333",
|
||||
/***/ "dark-kbd-border": "#555555",
|
||||
/**/ "light-kbd-border": "#CCCCCC",
|
||||
/***/ "dark-kbd-outer-shadow": "#FFFFFF4D",
|
||||
/**/ "light-kbd-outer-shadow": "#00000033",
|
||||
/***/ "dark-kbd-inner-shadow": "#000000",
|
||||
/**/ "light-kbd-inner-shadow": "#FFFFFF",
|
||||
|
||||
// mark
|
||||
/***/ "dark-mark-bg": "#FFFF0080",
|
||||
/**/ "light-mark-bg": "#FFFF00BF",
|
||||
/***/ "dark-mark-text": "#FFFFFF",
|
||||
/**/ "light-mark-text": "#000000",
|
||||
|
||||
// scrollbar
|
||||
/***/ "dark-scrollbar-track": "#18181B",
|
||||
/**/ "light-scrollbar-track": "#FFFFFF",
|
||||
/***/ "dark-scrollbar-thumb": "#888888",
|
||||
/**/ "light-scrollbar-thumb": "#DDDDDD",
|
||||
|
||||
// scroll progress
|
||||
/***/ "dark-scroll-progress-bg": "#52525B",
|
||||
/**/ "light-scroll-progress-bg": "#D4D4D8",
|
||||
/***/ "dark-scroll-progress-fg": "#D4D4D8",
|
||||
/**/ "light-scroll-progress-fg": "#52525B",
|
||||
|
||||
// table
|
||||
/***/ "dark-table-border": "#777777",
|
||||
/**/ "light-table-border": "#DDD",
|
||||
/***/ "dark-table-alt": "#21272E",
|
||||
/**/ "light-table-alt": "#F2F2F2",
|
||||
},
|
||||
fontFamily: {
|
||||
"noto-sans": ['"Noto Sans KR"', "sans-serif"],
|
||||
"source-code-pro": ['"Source Code Pro"', "monospace"],
|
||||
},
|
||||
boxShadow: {
|
||||
glow: "0 0px 10px",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
32
apps/portfolio/tsconfig.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* alias */
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
apps/portfolio/tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
13
apps/portfolio/vite.config.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import linaria from "@linaria/vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
import dynamicImport from "vite-plugin-dynamic-import"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), linaria(), dynamicImport(), tsconfigPaths()],
|
||||
build: {
|
||||
outDir: "dist",
|
||||
},
|
||||
})
|