feat: add portfolio site

This commit is contained in:
Kim, Jimin 2023-06-29 12:28:25 +09:00
parent 8090f62f1e
commit 4a6d765c86
80 changed files with 3891 additions and 1248 deletions

30
apps/portfolio/.eslintrc Normal file
View 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
View 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?

View file

@ -0,0 +1,5 @@
{
"semi": false,
"tabWidth": 4,
"plugins": ["prettier-plugin-tailwindcss"]
}

23
apps/portfolio/index.html Normal file
View 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>

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

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View 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

View 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

View file

@ -0,0 +1,3 @@
import Badge from "./Badge"
export default Badge

View file

@ -0,0 +1,11 @@
.light-badge {
svg {
@apply dark:fill-light-text-default;
}
}
.dark-badge {
svg {
@apply dark:fill-dark-text-default;
}
}

View 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

View file

@ -0,0 +1,3 @@
import Header from "./Header"
export default Header

View 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

View 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

View file

@ -0,0 +1,3 @@
import ProjectCard from "./ProjectCard"
export default ProjectCard

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

View 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

View file

@ -0,0 +1,3 @@
import Toc from "./Toc"
export default Toc

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

View 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 />
)

View 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

View file

@ -0,0 +1,3 @@
import Home from "./Home"
export default Home

View file

@ -0,0 +1,5 @@
.projects {
h2 {
@apply mb-4;
}
}

View 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

View file

@ -0,0 +1,3 @@
import NotFound from "./NotFound"
export default NotFound

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

View 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

View file

@ -0,0 +1,3 @@
import Project from "./Project"
export default Project

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

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

@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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: [],
}

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

View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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",
},
})