feat(portfolio): migrate to nextJS

This commit is contained in:
Kim, Jimin 2023-07-28 22:32:33 +09:00
parent 2d600d724d
commit 953379b5e8
Signed by: pomp
GPG key ID: CE1DDB8A4A765403
42 changed files with 476 additions and 2761 deletions

View file

@ -1,32 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"node": 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": {
"prettier/prettier": "error",
"react-refresh/only-export-components": "warn",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
}
}

View file

@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ["next/core-web-vitals", "@developomp-site/eslint-config"],
rules: {
"react-hooks/exhaustive-deps": "off",
"react/no-unescaped-entities": "off",
},
}

35
apps/portfolio/.gitignore vendored Normal file
View file

@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -1,26 +0,0 @@
<!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="developomp's portfolio" />
<meta property="og:description" content="developomp's Portfolio" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://portfolio.developomp.com" />
<meta
property="og:image"
content="https://portfolio.developomp.com/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,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
distDir: "dist",
images: { unoptimized: true },
experimental: {
externalDir: true,
},
}
module.exports = nextConfig

View file

@ -1,52 +1,47 @@
{
"name": "@developomp-site/portfolio",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint .",
"preview": "vite preview",
"clean": "rm -rf .turbo node_modules dist vite.config.ts.timestamp-*"
},
"dependencies": {
"@fontsource/noto-sans-kr": "^5.0.5",
"@fontsource/source-code-pro": "^5.0.5",
"@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"
"dev": "open-cli http://localhost:5174 && next dev -p 5174",
"build": "next build",
"lint": "next lint",
"clean": "rm -rf .next .turbo build node_modules"
},
"devDependencies": {
"@developomp-site/blog": "workspace:*",
"@developomp-site/content": "workspace:*",
"@developomp-site/eslint-config": "workspace:*",
"@developomp-site/prettier-config": "workspace:*",
"@developomp-site/tailwind-config": "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.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@vitejs/plugin-react": "^4.0.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.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.2",
"eslint-plugin-simple-import-sort": "^10.0.0",
"postcss": "^8.4.25",
"@fontsource/noto-sans-kr": "^5.0.5",
"@fontsource/source-code-pro": "^5.0.5",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@kunukn/react-collapse": "^2.2.10",
"@types/highlight.js": "^10.1.0",
"@types/katex": "^0.16.2",
"@types/node": "20.4.5",
"@types/react-collapse": "^5.0.1",
"@types/react-dom": "18.2.7",
"@types/react": "18.2.17",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"autoprefixer": "10.4.14",
"eslint-config-next": "13.4.12",
"eslint": "8.45.0",
"highlight.js": "^11.8.0",
"katex": "^0.16.8",
"next": "13.4.12",
"open-cli": "^7.2.0",
"postcss": "8.4.27",
"prettier-plugin-tailwindcss": "^0.4.1",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.6",
"vite": "^4.4.2",
"vite-plugin-dynamic-import": "^1.5.0",
"vite-tsconfig-paths": "^4.2.0"
"react-collapse": "^5.1.1",
"react-dom": "18.2.0",
"react": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.1.6"
}
}

View file

@ -1,4 +1,4 @@
export default {
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1,41 +0,0 @@
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 Loading from "@/routes/Loading"
import NotFound from "@/routes/NotFound"
import Project from "@/routes/Project"
const App: FC = () => {
useTitleTemplate("Portfolio | %s")
// no need to set title and meta tags here
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 path="/loading">
<Loading />
</Route>
<Route>
<NotFound />
</Route>
</Switch>
</div>
</>
)
}
export default App

View file

@ -11,11 +11,7 @@ html {
}
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;
@apply m-0 flex h-full min-h-screen w-full scroll-m-16 flex-col items-center p-0;
}
ul,

View file

@ -0,0 +1,85 @@
import "@fortawesome/fontawesome-svg-core/styles.css"
import "@fontsource/noto-sans-kr/400.css"
import "@fontsource/noto-sans-kr/700.css"
import "@fontsource/source-code-pro"
import "@developomp-site/blog/src/styles/anchor.scss"
import "@developomp-site/blog/src/styles/blockQuote.scss"
import "@developomp-site/blog/src/styles/button.scss"
import "@developomp-site/blog/src/styles/callout.scss"
import "@developomp-site/blog/src/styles/checkbox.scss"
import "@developomp-site/blog/src/styles/code.scss"
import "@developomp-site/blog/src/styles/colorChip.scss"
import "@developomp-site/blog/src/styles/heading.scss"
import "@developomp-site/blog/src/styles/hr.scss"
import "@developomp-site/blog/src/styles/img.scss"
import "@developomp-site/blog/src/styles/katex.scss"
import "@developomp-site/blog/src/styles/kbd.scss"
import "@developomp-site/blog/src/styles/list.scss"
import "@developomp-site/blog/src/styles/mark.scss"
import "@developomp-site/blog/src/styles/scrollbar.scss"
import "@developomp-site/blog/src/styles/subSup.scss"
import "@developomp-site/blog/src/styles/table.scss"
import "./global.scss"
import { type Metadata } from "next"
import Image from "next/image"
import Header from "@/components/Header"
export const metadata: Metadata = {
metadataBase: new URL("https://portfolio.developomp.com"),
title: {
template: "pomp's portfolio | %s",
default: "",
},
description: "developomp's portfolio",
openGraph: {
title: "pomp's portfolio",
siteName: "developomp's portfolio",
description: "developomp's portfolio",
type: "website",
url: "https://portfolio.developomp.com",
images: "https://portfolio.developomp.com/favicon.svg",
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="dark">
<head>
<link
rel="shortcut icon"
type="image/svg+xml"
href="/favicon.svg"
/>
<meta name="theme-color" content="#000000" />
</head>
<body>
<noscript>
<figure>
<Image
src="/img/nojs.avif"
height={500}
width={544}
alt="No javascript?"
/>
<figcaption>
Image compressed down to 4.5kB because you probably
have potato internet :D
</figcaption>
</figure>
</noscript>
<Header />
<div className="mb-10 mt-20 w-full max-w-screen-md px-4">
{children}
</div>
</body>
</html>
)
}

View file

@ -0,0 +1,21 @@
import { type Metadata } from "next"
export const metadata: Metadata = {
metadataBase: new URL("https://portfolio.developomp.com"),
title: "404",
openGraph: {
title: "pomp's portfolio | Page Not Found",
},
}
export default function NotFound() {
return (
<>
<h1 className="w-fit px-4 py-2 text-5xl dark:bg-dark-text-default dark:text-dark-ui-bg">
404
</h1>
<h2 className="glitch layers text-5xl">Page Not Found</h2>
</>
)
}

View file

@ -1,9 +1,6 @@
import "./style.scss"
import portfolio from "@developomp-site/content/dist/portfolio.json"
import type { PortfolioProject } from "@developomp-site/content/src/types/types"
import { useMeta, useTitle } from "hoofd"
import { type FC } from "react"
import { Metadata } from "next"
import Badge from "@/components/Badge"
import ProjectCard from "@/components/ProjectCard"
@ -27,10 +24,12 @@ for (const projectID in portfolio.projects) {
)
}
const Home: FC = () => {
useTitle("Home")
useMeta({ property: "og:title", content: "Home" })
export const metadata: Metadata = {
metadataBase: new URL("https://blog.developomp.com"),
title: "pomp's portfolio | Home",
}
export default function Page() {
return (
<>
<h1 className="mb-8">developomp's Portfolio</h1>
@ -40,5 +39,3 @@ const Home: FC = () => {
</>
)
}
export default Home

View file

@ -0,0 +1,97 @@
import "./style.scss"
import Toc from "@developomp-site/blog/src/app/[category]/[[...slug]]/Toc"
import portfolio from "@developomp-site/content/dist/portfolio.json"
import { faGithub } from "@fortawesome/free-brands-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { type Metadata } from "next"
import Link from "next/link"
import Badge from "@/components/Badge"
interface Data {
title: string
toc?: string
content: string
image: string // image url
overview: string
badges: string[]
repo: string
}
interface Params {
id: keyof typeof portfolio.projects
}
interface Props {
params: Params
}
async function getData(id: keyof typeof portfolio.projects): Promise<Data> {
const content = await import(
`@developomp-site/content/dist/content/projects/${id}.json`
)
const data = portfolio.projects[id]
return {
content: content.content,
toc: content.toc,
title: data.name,
image: data.image,
overview: data.overview,
badges: data.badges,
repo: data.repo,
}
}
export async function generateStaticParams(): Promise<Params[]> {
return (
Object.keys(portfolio.projects) as (keyof typeof portfolio.projects)[]
).map((id) => ({ id }))
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const data = await getData(params.id)
return {
metadataBase: new URL("https://portfolio.developomp.com"),
title: `pomp's portfolio | ${data.title}`,
openGraph: {
title: `pomp's portfolio | ${data.title}`,
},
}
}
export default async function Project({ params }: Props) {
const data = await getData(params.id)
return (
<>
<div className="flex items-center justify-between">
<h1 className="mb-4 text-4xl">{data.title}</h1>
<Link
href={data.repo}
className="text-dark-text-default duration-100 hover:text-gray-400"
>
<FontAwesomeIcon className="h-12" icon={faGithub} />
</Link>
</div>
<div className="flex flex-wrap">
{data.badges.map((slug) => {
return <Badge key={slug} slug={slug} />
})}
</div>
<hr />
<Toc data={data.toc} />
{/* page content */}
<div
className="project-description"
dangerouslySetInnerHTML={{
__html: data.content,
}}
/>
</>
)
}

View file

@ -1,3 +1,5 @@
"use client"
import "./style.scss"
import { type Badge as BadgeType } from "@developomp-site/content/src/types/types"

View file

@ -1,5 +1,5 @@
import Link from "next/link"
import { type FC } from "react"
import { Link } from "wouter"
const Header: FC = () => {
return (
@ -7,9 +7,10 @@ const Header: FC = () => {
<div className="my-0 flex h-16 w-full max-w-5xl items-center">
<Link
className="flex items-center"
to="/"
href="/"
aria-label="homepage"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="m-4 block h-10 cursor-pointer"
src="/favicon.svg"

View file

@ -1,13 +0,0 @@
import "./style.scss"
import { type FC } from "react"
const Loading: FC = () => {
return (
<div className="loading flex animate-bounce flex-col items-center justify-center text-center">
<h2 className="text-3xl">Loading...</h2>
</div>
)
}
export default Loading

View file

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

View file

@ -1,18 +0,0 @@
.loading {
@apply transition-opacity ease-in;
animation: fade-in 2s;
@keyframes fade-in {
0% {
opacity: 0;
}
25% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}

View file

@ -1,7 +1,7 @@
import "./style.scss"
import { PortfolioProject } from "@developomp-site/content/src/types/types"
import { type FC, useEffect, useState } from "react"
import Link from "next/link"
import Badge from "@/components/Badge"
@ -10,26 +10,23 @@ interface ProjectCardProps {
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])
export default function ProjectCard({ projectID, project }: ProjectCardProps) {
return (
<a href={`/project/${projectID}`}>
<Link href={`/project/${projectID}`}>
<div className="project">
<h2>{project.name}</h2>
<h2 className="mb-4">{project.name}</h2>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="mb-4 w-full object-cover"
src={project.image}
alt="project thumbnail"
/>
<div className="flex flex-wrap">{badges}</div>
<div className="flex flex-wrap">
{project.badges.map((badge) => (
<Badge key={badge} slug={badge} />
))}
</div>
<hr className="my-1" />
<div
dangerouslySetInnerHTML={{
@ -37,8 +34,6 @@ const ProjectCard: FC<ProjectCardProps> = ({ projectID, project }) => {
}}
/>
</div>
</a>
</Link>
)
}
export default ProjectCard

View file

@ -1,68 +0,0 @@
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;
}
`
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

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

View file

@ -1,18 +0,0 @@
import "@fontsource/noto-sans-kr/400.css"
import "@fontsource/noto-sans-kr/700.css"
import "@fontsource/source-code-pro"
import "./index.scss"
import "@developomp-site/blog/src/styles/anchor.scss"
import "@developomp-site/blog/src/styles/callout.scss"
import "@developomp-site/blog/src/styles/colorChip.scss"
import "@developomp-site/blog/src/styles/heading.scss"
import "@developomp-site/blog/src/styles/img.scss"
import "@developomp-site/blog/src/styles/mark.scss"
import ReactDOM from "react-dom/client"
import App from "./App.tsx"
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<App />
)

View file

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

View file

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

View file

@ -1,13 +0,0 @@
import { useMeta, useTitle } from "hoofd"
import { type FC } from "react"
import Loading from "@/components/Loading"
const LoadingPage: FC = () => {
useTitle("Loading")
useMeta({ property: "og:title", content: "Loading" })
return <Loading />
}
export default LoadingPage

View file

@ -1,26 +0,0 @@
import "./style.css"
import { useMeta, useTitle } from "hoofd"
import { type FC } from "react"
const NotFound: FC = () => {
useTitle("404")
useMeta({ property: "og:title", content: "Page Not Found" })
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

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

View file

@ -1,389 +0,0 @@
/* 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

@ -1,104 +0,0 @@
import "./style.scss"
import portfolio from "@developomp-site/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/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 (
<>
<div className="flex items-center justify-between">
<h1 className="mb-4 text-4xl">{pageData.title}</h1>
{/* <!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> */}
<a href={pageData.repo}>
<svg
className="h-12 fill-dark-text-default duration-100 hover:fill-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 496 512"
>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg>
</a>
</div>
<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

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

View file

@ -1,47 +0,0 @@
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")
})

View file

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

View file

@ -1,6 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
presets: [require("@developomp-site/tailwind-config/tailwind.config")],
darkMode: "class",
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
module.exports = {
presets: [require("@developomp-site/tailwind-config/tailwind.config.js")],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
}

View file

@ -1,32 +1,35 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* Bundler mode */
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* alias */
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"build/types/**/*.ts",
"dist/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View file

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

View file

@ -1,17 +0,0 @@
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",
},
server: {
open: true,
port: 5174,
},
})