Compare commits

...
43 Commits
Author SHA1 Message Date
AramJonghu 208299b3d8 fix(contact): xmpp address typo 2026-08-11 14:03:43 +02:00
AramJonghu 0d78d32144 chore: update package.json 2026-08-11 12:37:03 +02:00
AramJonghu ad7e151618 chore(projects): removal of projects section on home page 2026-08-11 12:36:10 +02:00
AramJonghu 28b2fff3d6 chore: audit fix and temporary removal ts7 from package 2026-08-11 12:10:34 +02:00
AramJonghu 6d44d8d1e9 chore(npm): audit fix and resolve dependency hell 2026-08-10 01:27:42 +02:00
AramJonghu abd43cefdd chore(contact): Updated contact page with some context flavour text 2026-08-08 18:42:05 +02:00
AramJonghu d2cc733386 chore(cleanup): Cleaning up some double or irrelevant information 2026-08-08 18:40:43 +02:00
AramJonghu 0611e56399 chore(mobile): mobile view improved /w min-w and component order 2026-08-07 06:11:09 +02:00
AramJonghu 7777afae64 feat(avatar): added avatar to portfolio 2026-08-06 22:53:58 +02:00
AramJonghu 4388242241 chore(styling): header and footer flexbox styling fix 2026-08-06 22:43:18 +02:00
AramJonghu fec4ed568c feat(Portfolio): implementation of Portfolio page 2026-08-06 22:19:04 +02:00
AramJonghu 030ca4009c feat(Timeline): Timeline component showing current entry. Might change. 2026-08-06 22:19:04 +02:00
AramJonghu 4a190627cb feat(portfolio sectioning): sectioning component of Portfolio.tsx 2026-08-06 22:19:04 +02:00
AramJonghu 7668100fb6 feat(skills): lists skills component of portfolio 2026-08-06 22:19:04 +02:00
AramJonghu 8e4f01b213 feat(content): portfolio content 2026-08-06 22:19:04 +02:00
AramJonghu c08ffdcb5e chore(footer): reordering and removal of github icon 2026-08-06 22:19:04 +02:00
AramJonghu a38f1b64b6 chore(redirects): added more icons and moved redirects to footer 2026-08-06 22:19:04 +02:00
AramJonghu 30476dbc27 chore(shiki): removed languages currently improbable to be part needed 2026-08-04 03:45:38 +02:00
AramJonghu 9993c9dcfa chore(link): LISCENSE link direct 2026-08-01 17:25:12 +02:00
AramJonghu b764d1c86b chore(npm): alias so eslint does not break 2026-08-01 17:21:37 +02:00
AramJonghu f8953a5d7b update(deps): npm deps updated 2026-08-01 17:11:23 +02:00
AramJonghu dc94a50354 chore(activity): updated 2026-07-31 22:03:07 +02:00
AramJonghu 3a42489556 feat(hamburger_menu): added mobile friendly hamburger menu 2026-07-31 21:53:38 +02:00
AramJonghu 4e91ecafec chore(prettier): prettier format 2026-07-31 21:27:39 +02:00
AramJonghu 34e1387aa8 feat(contacts+css): minor css changes + contacts page 2026-07-31 21:26:51 +02:00
AramJonghu 5d07d33eaa chore(mobile view): more mobile view improvements e.g. item sizes 2026-07-31 20:23:38 +02:00
AramJonghu 9edde535bb feat(mobile): improve mobile view by introducing css for small devices 2026-07-31 18:30:57 +02:00
AramJonghu 6b94a75e30 feat(default): default page changed to about page 2026-07-29 06:41:46 +02:00
AramJonghu 046c2f395a chore(npm): audit fix 2026-07-29 06:39:37 +02:00
AramJonghu 605685abb3 feat(about): new language & tools section (incomplete) 2026-07-29 06:38:25 +02:00
AramJonghu b629e042b2 chore(ci): on push ci vs on pr 2026-07-29 05:04:34 +02:00
AramJonghu f91587a767 chore(cleanup): removed double title card text; less wasted space 2026-07-23 03:29:05 +02:00
AramJonghu d2b2eb2343 perf(highlighter): lazy-init Shiki at module level without blocking 2026-07-22 16:14:28 +02:00
AramJonghu e613d96ddf chore(nesting): reduced nesting 2026-07-22 00:01:39 +02:00
AramJonghu 21c61968fe chore(background): project layout background is now fitting 2026-07-21 23:51:03 +02:00
AramJonghu f583696295 chore(icons): nerd-fonts icons included 2026-07-21 23:47:55 +02:00
AramJonghu bfc7dd9018 chore(README): updated README to fit changes 2026-07-21 23:26:27 +02:00
AramJonghu 9041c683f8 feat(codeblocks): codeblocks now have proper theming with highlighting 2026-07-21 23:24:16 +02:00
AramJonghu 1311c79479 feat(font): inter default font, monospace untouched 2026-07-20 18:59:33 +02:00
AramJonghu b1cc3a837a chore(format): prettier format 2026-07-20 17:46:08 +02:00
AramJonghu d489adee3c Chore(refactor): refactor project info into separate component 2026-07-20 16:54:45 +02:00
AramJonghu e16833ecc3 chore(readme): adds LICENSE link in README 2026-07-19 22:14:53 +02:00
AramJonghu d66133082c chore(project): new project added 2026-07-19 20:45:46 +02:00
31 changed files with 1136 additions and 369 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Format, lint, typecheck & build
on:
pull_request:
push:
branches: ["master"]
jobs:
+38 -29
View File
@@ -8,35 +8,38 @@ This site is not just a portfolio — it acts as an entry point into a larger ec
## Overview
The site is divided into three conceptual layers:
The site is divided into two conceptual layers:
### 1. Home
### 1. Tabs (Home)
The homepage provides a high-level overview of who I am and what I work on.
It highlights selected projects and core skills, without listing everything.
The homepage provides tabs for three sections:
- **Projects** — fetches and renders READMEs from self-hosted Forgejo repositories via raw URL
- **Current-activity** — language learning progress and active work
- **About** — personal background, philosophy, and Q&A
### 2. Header
The header serves as a navigation layer into my self-hosted infrastructure.
Each entry links to a running instance.
The header serves as a navigation layer into my self-hosted infrastructure. Each entry links to a running instance.
- Nextcloud — file storage and synchronization
- Navidrome — personal music streaming
- Forgejo — Git hosting and development platform
- OmniSearch — privacy-focused search engine
- Stream service — live media platform powered by MediaMTX
- Friends Gitea instance — external development collaboration
- Forgejo Git — Git hosting and development platform
- Stream — live media platform powered by MediaMTX (internal route)
- Omnisearch — privacy-focused search engine
- Gitea zach-dev — external development collaboration
These services are part of my personal infrastructure and exist outside the portfolio itself.
## Stack
### 3. Pages
Dedicated pages for deeper content:
- Projects — selected work and builds
- Systems — architecture and self-hosted infrastructure overview
- About — personal background and context
- CV — formal resume (if applicable)
| Layer | Choice | Notes |
| --------- | ---------------------------- | ----------------------------------------- |
| Language | TypeScript / React 19 | Vite 8 + rolldown |
| CSS | TailwindCSS v4 | Catppuccin macchiato/latte |
| Markdown | react-markdown v10 | remark-gfm, rehype-raw, rehype-sanitize |
| Syntax HL | Shiki v4 | Catppuccin themes, CSS variable switching |
| Font | Inter (self-hosted, 4 WOFF2) | no Google Fonts |
| Theme | Catppuccin macchiato/latte | JS toggle, persisted to localStorage |
| Git host | Forgejo (self-hosted) | git.aramjonghu.dev |
## Philosophy
@@ -51,16 +54,22 @@ Instead of a traditional portfolio, it focuses on:
It evolves alongside my personal development and technical interests.
## Stack
## Project layout
- React
- Vite
- Tailwind CSS
- Catppuccin theme
- Self-hosted backend services (Forgejo, Nextcloud, etc.)
```
├── public/ favicon
├── src/
│ ├── assets/ images, fonts, logo
│ ├── components/ MdProcessor, Header, Layout, ProjectInfo, ...
│ ├── content/ about.md, current-activity.md, projects.json
│ ├── hooks/ useTheme
│ ├── pages/ Home
│ └── utils/ theme, highlighter
├── index.html
├── package.json
└── LICENSE
```
## Status
## License
This site is continuously evolving and serves as both a portfolio and experimental environment.
Expect structural changes as systems grow.
BSD 3-Clause. See [LICENSE](https://git.aramjonghu.dev/AramJonghu/aramjonghu-site/src/branch/master/LICENSE).
+9 -8
View File
@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@shikijs/rehype": "^4.3.1",
"@tailwindcss/vite": "^4.2.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -18,26 +19,26 @@
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"tailwindcss": "^4.2.2"
"shiki": "^4.3.1",
"tailwindcss": "^4.2.2",
"typescript-eslint": "^8.67.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/core": "^8.0.1",
"@catppuccin/tailwindcss": "^1.0.0",
"@eslint/js": "^9.39.4",
"@eslint/js": "^10.0.1",
"@rolldown/plugin-babel": "^0.2.2",
"@tailwindcss/language-server": "^0.14.29",
"@tailwindcss/language-server": "^0.16.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.39.4",
"eslint": "^10.8.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"prettier": "^3.8.3",
"typescript": "^5.7.3",
"typescript-eslint": "^8.61.1",
"typescript-language-server": "^5.3.0",
"typescript": "^7.0.2",
"vite": "^8.0.4",
"vscode-langservers-extracted": "^4.10.0"
},
+63 -1
View File
@@ -1,8 +1,54 @@
@import "tailwindcss";
@import "@catppuccin/tailwindcss/macchiato.css";
@theme {
--font-family-sans: Inter, ui-sans-serif, system-ui, sans-serif;
}
@font-face {
font-family: "Inter";
src: url("./assets/fonts/Inter-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("./assets/fonts/Inter-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("./assets/fonts/Inter-SemiBold.woff2") format("woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("./assets/fonts/Inter-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "NerdFontSymbols";
src: url("./assets/fonts/nerd-fonts-symbols.woff2") format("woff2");
font-display: block;
}
.ui-btn {
@apply text-ctp-text border border-ctp-text rounded px-3 py-1 cursor-pointer transition-colors;
@apply text-ctp-text text-sm border border-ctp-text rounded px-3 py-1 min-w-8 lg:min-w-26 text-center cursor-pointer transition-colors;
}
.theme-ui-btn {
@apply text-ctp-text text-3xl border border-ctp-text rounded px-3 py-1 min-w-10 text-center cursor-pointer transition-colors;
}
.ui-btn:hover {
@@ -22,3 +68,19 @@
.stream-frame iframe {
@apply w-full h-full border-0;
}
.shiki,
.shiki .line span {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
}
.macchiato .shiki,
.macchiato .shiki .line span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
}
.macchiato .icon-invert {
filter: invert(1);
}
+11 -3
View File
@@ -4,8 +4,10 @@ import "./App.css";
import Layout from "./components/Layout";
import Home from "./pages/Home";
import Stream from "./pages/Stream";
import Contact from "./pages/Contact";
import Portfolio from "./pages/Portfolio";
function App(): ReactElement {
export default function App(): ReactElement {
const path = window.location.pathname.toLowerCase();
let content: ReactElement = <Home />;
@@ -13,7 +15,13 @@ function App(): ReactElement {
content = <Stream />;
}
if (path === "/contact") {
content = <Contact />;
}
if (path === "/portfolio") {
content = <Portfolio />;
}
return <Layout>{content}</Layout>;
}
export default App;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>github [#142]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#000000">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+38
View File
@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
viewBox="0 0 256 256" width="256" height="256">
<style>
.st1,.st2{fill:#000;stroke:#fff;stroke-width:9.9999;stroke-miterlimit:15.118}
.st2{fill:none;stroke-linecap:round}
</style>
<g id="spinGroup">
<circle cx="128" cy="128" r="122.8"
style="fill:#0084ff;stroke:#fff;stroke-width:10.3641;stroke-miterlimit:15.118"/>
<circle cx="128" cy="128" r="43.7" fill="#fff" class="st1"/>
<path fill="#fff"
d="M201.8 128c0 40.7-33 73.8-73.8 73.8M54.2 128c0-40.7 33-73.8 73.8-73.8"
class="st2"/>
<path fill="#fff"
d="M224.9 128c0 1.2 0 2.5-.1 3.7m-2.2 17.6c-9.7 43.3-48.4 75.6-94.6 75.6
M31.1 128c0-1.1 0-2.2.1-3.3m1.8-16c9-44.2 48.1-77.6 95-77.6"
class="st2"/>
<circle cx="128" cy="128" r="9" fill="#fff" class="st1"/>
<!-- SPIN ANIMATION INSIDE THE GROUP -->
<animateTransform
attributeName="transform"
type="rotate"
from="0 128 128"
to="360 128 128"
dur="3s"
repeatCount="indefinite"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#0082c9"/>
<path d="M512.5 370.9c-64.6 0-118.8 44.2-135.4 103.7-14.5-31.8-46.3-54.4-83.3-54.4-50.6.2-91.5 41.2-91.8 91.8.2 50.6 41.2 91.5 91.8 91.8 37 0 68.8-22.6 83.3-54.4 16.6 59.5 70.8 103.7 135.4 103.7 64.2 0 118.2-43.7 135.1-102.7 14.7 31.3 46.1 53.4 82.6 53.4 50.6-.2 91.6-41.2 91.8-91.8-.2-50.6-41.2-91.6-91.8-91.8-36.6 0-67.9 22.2-82.6 53.4-16.9-59-70.9-102.8-135.1-102.7zm0 53.8c48.5 0 87.3 38.8 87.3 87.3s-38.8 87.3-87.3 87.3c-48 .2-87-38.5-87.2-86.5v-.8c-.1-48.5 38.7-87.3 87.2-87.3zm-218.7 49.4c21.2 0 37.9 16.7 37.9 37.9 0 21.3-16.6 37.9-37.9 37.9-20.7.2-37.7-16.4-37.9-37.1v-.8c0-21.3 16.6-37.9 37.9-37.9zm436.4 0c21.3 0 37.9 16.7 37.9 37.9 0 21.3-16.7 37.9-37.9 37.9-20.7.2-37.7-16.4-37.9-37.1v-.8c0-21.3 16.7-37.9 37.9-37.9z" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-0.5 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11.8201C22 9.84228 21.4135 7.90885 20.3147 6.26436C19.2159 4.61987 17.6542 3.33813 15.8269 2.58126C13.9996 1.82438 11.9889 1.62637 10.0491 2.01223C8.10927 2.39808 6.32748 3.35052 4.92896 4.74904C3.53043 6.14757 2.578 7.92935 2.19214 9.86916C1.80629 11.809 2.00436 13.8197 2.76123 15.6469C3.51811 17.4742 4.79985 19.036 6.44434 20.1348C8.08883 21.2336 10.0222 21.8201 12 21.8201" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 11.8201H22" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 21.8201C10.07 21.8201 8.5 17.3401 8.5 11.8201C8.5 6.30007 10.07 1.82007 12 1.82007C13.93 1.82007 15.5 6.30007 15.5 11.8201" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3691 21.6901C20.3021 21.6901 21.8691 20.1231 21.8691 18.1901C21.8691 16.2571 20.3021 14.6901 18.3691 14.6901C16.4361 14.6901 14.8691 16.2571 14.8691 18.1901C14.8691 20.1231 16.4361 21.6901 18.3691 21.6901Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22.9998 22.8202L20.8398 20.6702" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

-3
View File
@@ -5,9 +5,6 @@ import aboutMd from "../content/about.md?raw";
export default function About(): ReactElement {
return (
<>
<h1 className="text-center m-6 text-3xl font-bold text-ctp-mauve">
About
</h1>
<MdProcessor content={aboutMd} />
</>
);
@@ -5,9 +5,6 @@ import currentActivityMd from "../content/current-activity.md?raw";
export default function CurrentActivity(): ReactElement {
return (
<>
<h1 className="text-center m-6 text-3xl font-bold text-ctp-mauve">
Current activity
</h1>
<MdProcessor content={currentActivityMd} />
</>
);
+60 -31
View File
@@ -1,42 +1,71 @@
import type { ReactElement } from "react";
import nextcloud from "../assets/images/nextcloud.svg";
import navidrome from "../assets/images/navidrome.svg";
import forgejo from "../assets/images/forgejo.svg";
import gitea from "../assets/images/gitea.svg";
import search from "../assets/images/search.svg";
export default function Footer() {
interface IconLink {
label: string;
url: string;
src: string;
invert?: boolean;
}
const iconLinks: IconLink[] = [
{
label: "Nextcloud",
url: "https://nextcloud.aramjonghu.dev",
src: nextcloud,
},
{ label: "Navidrome", url: "https://music.aramjonghu.dev", src: navidrome },
{
label: "Forgejo Git",
url: "https://git.aramjonghu.dev/AramJonghu",
src: forgejo,
},
{
label: "Omnisearch",
url: "https://xng.aramjonghu.dev",
src: search,
invert: true,
},
{
label: "Gitea zach-dev",
url: "https://git.zach-dev.cc/AramJonghu",
src: gitea,
},
];
export default function Footer(): ReactElement {
const currentYear = new Date().getFullYear();
const goToForgejo = () => {
window.location.href =
"https://git.aramjonghu.dev/AramJonghu/aramjonghu-site";
};
const goToGitea = () => {
window.location.href =
"https://git.zach-dev.cc/AramJonghu/aramjonghu-site";
};
const startYear = 2025;
return (
<footer className="bg-ctp-mantle flex items-center justify-between px-24 py-6 border-t border-ctp-lavender-800 mt-10">
<footer className="bg-ctp-mantle flex flex-col md:flex-row items-center justify-around gap-3 px-4 md:px-24 py-6 border-t border-ctp-lavender-800 mt-10">
<p className="text-sm text-ctp-text">
&copy; {currentYear} AramJonghu. All rights reserved.
&copy; {startYear} - {currentYear} AramJonghu. All rights
reserved.
</p>
<div className="flex">
<p className="text-sm text-ctp-text">
Check out the website repository here:
</p>
<img
src={forgejo}
alt="Forgejo repository"
onClick={goToForgejo}
className="ml-2 w-6 h-6 cursor-pointer hover:opacity-80 transition-opacity"
/>
<img
src={gitea}
alt="Gitea mirror"
onClick={goToGitea}
className="ml-2 w-6 h-6 cursor-pointer hover:opacity-80 transition-opacity"
/>
<div className="flex items-center gap-2">
{iconLinks.map((icon) => (
<a
key={icon.label}
href={icon.url}
target="_blank"
rel="noreferrer"
aria-label={icon.label}
title={icon.label}
className="p-2 cursor-pointer hover:opacity-80 transition-opacity"
>
<img
src={icon.src}
alt={icon.label}
className={`w-7 h-7 ${icon.invert ? "icon-invert" : ""}`}
/>
</a>
))}
</div>
</footer>
);
+78 -24
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import Aku from "../assets/images/aku.png";
interface NavItem {
interface PageItem {
label: string;
url: string;
}
@@ -16,46 +17,99 @@ interface HeaderProps {
export default function Header({ theme }: HeaderProps) {
const { theme: currentTheme, toggleTheme } = theme;
const [menuOpen, setMenuOpen] = useState(false);
const navItems: NavItem[] = [
{ label: "Nextcloud", url: "https://nextcloud.aramjonghu.dev" },
{ label: "Navidrome", url: "https://music.aramjonghu.dev" },
{ label: "Forgejo Git", url: "https://git.aramjonghu.dev" },
const pageItems: PageItem[] = [
{ label: "Stream", url: "/stream" },
{ label: "Omnisearch", url: "https://xng.aramjonghu.dev" },
{ label: "Gitea zach-dev", url: "https://git.zach-dev.cc" },
{ label: "Portfolio", url: "/portfolio" },
{ label: "Contact", url: "/contact" },
];
const goHome = () => {
window.location.href = "/";
};
return (
<header className="bg-ctp-mantle flex justify-between items-center px-12 py-6 gap-6 border-b border-ctp-lavender-800">
<img
className="size-16 cursor-pointer"
src={Aku}
alt="Home"
onClick={goHome}
/>
const closeMenu = () => {
setMenuOpen(false);
};
<nav className="flex flex-wrap gap-3 items-center">
{navItems.map((item) => (
return (
<header className="bg-ctp-mantle flex flex-col md:flex-row justify-evenly items-start md:items-center px-4 md:px-12 py-4 md:py-6 gap-4 md:gap-6 border-b border-ctp-lavender-800">
<div className="grid grid-cols-3 items-center w-full md:hidden gap-4">
<img
className="size-16 cursor-pointer justify-self-start"
src={Aku}
alt="Home"
onClick={goHome}
/>
<button
className="theme-ui-btn ui-btn:hover font-[NerdFontSymbols] justify-self-center"
aria-label="Toggle menu"
aria-expanded={menuOpen}
onClick={() => setMenuOpen(!menuOpen)}
>
{menuOpen ? "\uf00d" : "\uf0c9"}
</button>
<button
className="theme-ui-btn ui-btn:hover text-lg font-[NerdFontSymbols] justify-self-end"
onClick={toggleTheme}
>
{currentTheme === "macchiato" ? "" : ""}
</button>
</div>
<div className="hidden md:block">
<img
className="size-16 cursor-pointer"
src={Aku}
alt="Home"
onClick={goHome}
/>
</div>
<nav className="hidden md:flex flex-wrap gap-2 md:gap-3 items-end">
{pageItems.map((page) => (
<a
key={item.label}
href={item.url}
className="ui-btn ui-btn:hover"
key={page.label}
href={page.url}
className="ui-btn ui-btn:hover px-2 py-1 md:px-3 md:py-1"
>
{item.label}
{page.label}
</a>
))}
</nav>
<div>
<button className="ui-btn ui-btn:hover" onClick={toggleTheme}>
{currentTheme === "macchiato" ? "🌙 Dark" : "☀️ Light"}
<div className="hidden md:block">
<button
className="theme-ui-btn ui-btn:hover text-lg font-[NerdFontSymbols]"
onClick={toggleTheme}
>
{currentTheme === "macchiato" ? "" : ""}
</button>
</div>
<div
className={`md:hidden w-full overflow-hidden transition-all duration-150 ease-in-out ${
menuOpen
? "max-h-80 opacity-100 visible"
: "max-h-0 opacity-0 invisible"
}`}
>
<nav className="flex flex-col gap-2 w-full max-w-sm mx-auto border-t border-ctp-lavender-800 pt-4">
{pageItems.map((item) => (
<a
key={item.label}
href={item.url}
className="ui-btn ui-btn:hover px-2 py-1 w-full"
onClick={closeMenu}
>
{item.label}
</a>
))}
</nav>
</div>
</header>
);
}
+239 -189
View File
@@ -4,11 +4,78 @@ import {
useMemo,
type ComponentPropsWithoutRef,
type ReactElement,
type ReactNode,
} from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { getHighlighter } from "../utils/highlighter";
function CopyButton({ code }: { code: string }): ReactElement {
return (
<button
onClick={(e) => {
const btn = e.currentTarget;
navigator.clipboard.writeText(code).then(() => {
btn.textContent = "Copied!";
setTimeout(() => {
btn.textContent = "\uf0c5";
}, 1500);
});
}}
className="absolute top-1 right-1 px-2 py-1 text-base leading-none text-ctp-subtext0 hover:text-ctp-green transition-colors cursor-pointer select-none z-10 font-[NerdFontSymbols]"
>
</button>
);
}
function HighlightedBlock({
code,
lang,
children,
}: {
code: string;
lang: string;
children: ReactNode;
}): ReactElement {
const [html, setHtml] = useState<string | null>(null);
useEffect(() => {
getHighlighter().then((hl) => {
setHtml(
hl.codeToHtml(code, {
lang,
themes: {
dark: "catppuccin-macchiato",
light: "catppuccin-latte",
},
defaultColor: false,
}),
);
});
}, [code, lang]);
return (
<div className="relative border-2 border-ctp-text/50 rounded-md overflow-hidden my-6 bg-ctp-base">
<span className="absolute top-0 left-0 px-2 py-1 text-xs font-mono text-ctp-mauve">
{lang}
</span>
<CopyButton code={code} />
{html ? (
<div
className="overflow-x-auto text-sm p-4 pt-7"
dangerouslySetInnerHTML={{ __html: html }}
/>
) : (
<pre className="overflow-x-auto text-sm p-4 pt-7 m-0">
{children}
</pre>
)}
</div>
);
}
const toRawUrl = (repoUrl: string): string => {
if (!repoUrl) return repoUrl;
@@ -110,6 +177,177 @@ export default function MdProcessor({
const loading = useRemote && fetchState.url !== repoUrl;
const displayContent = directContent ?? fetchState.content;
const markdownComponents = {
code({
className,
children,
...props
}: ComponentPropsWithoutRef<"code">): ReactElement {
const isBlock =
className ||
(typeof children === "string" && children.includes("\n"));
if (isBlock) {
return (
<code className="text-sm" {...props}>
{children}
</code>
);
}
return (
<code
className="bg-ctp-mantle px-1.5 py-0.5 rounded text-sm font-mono"
{...props}
>
{children}
</code>
);
},
pre({ children }: ComponentPropsWithoutRef<"pre">): ReactElement {
const codeChild = Array.isArray(children) ? children[0] : children;
let lang = "";
let code = "";
if (
codeChild &&
typeof codeChild === "object" &&
"props" in codeChild
) {
const el = codeChild as {
props: { className?: string; children?: string };
};
const cls = el.props?.className;
const match = cls?.match(/language-(\w+)/);
if (match) lang = match[1];
if (typeof el.props?.children === "string") {
code = el.props.children;
}
}
if (lang && code) {
return (
<HighlightedBlock code={code} lang={lang}>
{children}
</HighlightedBlock>
);
}
return (
<div className="relative border-2 border-ctp-text/50 rounded-md overflow-hidden my-6 bg-ctp-base">
<CopyButton code={code} />
<pre className="overflow-x-auto text-sm p-4 m-0">
{children}
</pre>
</div>
);
},
h1({ children }: ComponentPropsWithoutRef<"h1">): ReactElement {
return (
<h1 className="text-3xl font-bold text-ctp-mauve mt-8 mb-4 border-b pb-2 border-ctp-green">
{children}
</h1>
);
},
h2({ children }: ComponentPropsWithoutRef<"h2">): ReactElement {
return (
<h2 className="text-2xl font-bold text-ctp-lavender mt-8 mb-4 border-b pb-2 border-ctp-green">
{children}
</h2>
);
},
h3({ children }: ComponentPropsWithoutRef<"h3">): ReactElement {
return (
<h3 className="text-xl font-bold text-ctp-lavender mt-5 mb-3">
{children}
</h3>
);
},
p({ children }: ComponentPropsWithoutRef<"p">): ReactElement {
return <p className="mb-4 text-ctp-text">{children}</p>;
},
ul({ children }: ComponentPropsWithoutRef<"ul">): ReactElement {
return <ul className="list-disc pl-6 mb-4">{children}</ul>;
},
ol({ children }: ComponentPropsWithoutRef<"ol">): ReactElement {
return <ol className="list-decimal pl-6 mb-4">{children}</ol>;
},
li({ children }: ComponentPropsWithoutRef<"li">): ReactElement {
return <li className="mb-1">{children}</li>;
},
a({ children, href }: ComponentPropsWithoutRef<"a">): ReactElement {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="hover:underline text-ctp-green"
>
{children}
</a>
);
},
blockquote({
children,
}: ComponentPropsWithoutRef<"blockquote">): ReactElement {
return (
<blockquote className="border-l-4 border-ctp-text pl-4 py-1 mb-4 bg-ctp-mantle rounded-r-lg">
{children}
</blockquote>
);
},
table({ children }: ComponentPropsWithoutRef<"table">): ReactElement {
return (
<div className="overflow-x-auto my-4">
<table className="w-full border-collapse border border-ctp-overlay0">
{children}
</table>
</div>
);
},
th({ children }: ComponentPropsWithoutRef<"th">): ReactElement {
return (
<th className="border border-ctp-overlay0 bg-ctp-surface0 px-4 py-2 text-left font-bold text-ctp-lavender">
{children}
</th>
);
},
td({ children }: ComponentPropsWithoutRef<"td">): ReactElement {
return (
<td className="border border-ctp-overlay0 px-4 py-2 text-ctp-text">
{children}
</td>
);
},
img({ src, alt }: ComponentPropsWithoutRef<"img">): ReactElement {
const resolved = resolveImageSrc(src || "", base);
return (
<img
src={resolved}
alt={alt}
className="max-w-full h-auto rounded-md my-4 shadow-sm border-2 border-ctp-text"
loading="lazy"
/>
);
},
};
return (
<div className="w-full max-w-4xl mx-auto p-4 md:p-8 bg-ctp-mantle rounded-xl shadow-sm border border-ctp-text">
{loading ? (
@@ -123,195 +361,7 @@ export default function MdProcessor({
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={{
code({
className,
children,
...props
}: ComponentPropsWithoutRef<"code">): ReactElement {
const isBlock =
className ||
(typeof children === "string" &&
children.includes("\n"));
if (isBlock) {
return (
<code
className={`${className || ""} text-sm block overflow-x-auto`}
{...props}
>
{children}
</code>
);
}
return (
<code
className="bg-ctp-mantle px-1.5 py-0.5 rounded text-sm font-mono"
{...props}
>
{children}
</code>
);
},
pre({
children,
...props
}: ComponentPropsWithoutRef<"pre">): ReactElement {
return (
<pre
className="bg-ctp-mantle p-4 rounded-lg overflow-x-auto text-sm my-4"
{...props}
>
{children}
</pre>
);
},
h1({
children,
}: ComponentPropsWithoutRef<"h1">): ReactElement {
return (
<h1 className="text-3xl font-bold text-ctp-mauve mt-8 mb-4 border-b pb-2 border-ctp-green">
{children}
</h1>
);
},
h2({
children,
}: ComponentPropsWithoutRef<"h2">): ReactElement {
return (
<h2 className="text-2xl font-bold text-ctp-lavender mt-8 mb-4 border-b pb-2 border-ctp-green">
{children}
</h2>
);
},
h3({
children,
}: ComponentPropsWithoutRef<"h3">): ReactElement {
return (
<h3 className="text-xl font-bold text-ctp-lavender mt-5 mb-3">
{children}
</h3>
);
},
p({
children,
}: ComponentPropsWithoutRef<"p">): ReactElement {
return (
<p className="mb-4 text-ctp-text">
{children}
</p>
);
},
ul({
children,
}: ComponentPropsWithoutRef<"ul">): ReactElement {
return (
<ul className="list-disc pl-6 mb-4">
{children}
</ul>
);
},
ol({
children,
}: ComponentPropsWithoutRef<"ol">): ReactElement {
return (
<ol className="list-decimal pl-6 mb-4">
{children}
</ol>
);
},
li({
children,
}: ComponentPropsWithoutRef<"li">): ReactElement {
return <li className="mb-1">{children}</li>;
},
a({
children,
href,
}: ComponentPropsWithoutRef<"a">): ReactElement {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="hover:underline text-ctp-green"
>
{children}
</a>
);
},
blockquote({
children,
}: ComponentPropsWithoutRef<"blockquote">): ReactElement {
return (
<blockquote className="border-l-4 border-ctp-text pl-4 py-1 mb-4 bg-ctp-mantle rounded-r-lg">
{children}
</blockquote>
);
},
table({
children,
}: ComponentPropsWithoutRef<"table">): ReactElement {
return (
<div className="overflow-x-auto my-4">
<table className="w-full border-collapse border border-ctp-overlay0">
{children}
</table>
</div>
);
},
th({
children,
}: ComponentPropsWithoutRef<"th">): ReactElement {
return (
<th className="border border-ctp-overlay0 bg-ctp-surface0 px-4 py-2 text-left font-bold text-ctp-lavender">
{children}
</th>
);
},
td({
children,
}: ComponentPropsWithoutRef<"td">): ReactElement {
return (
<td className="border border-ctp-overlay0 px-4 py-2 text-ctp-text">
{children}
</td>
);
},
img({
src,
alt,
}: ComponentPropsWithoutRef<"img">): ReactElement {
const resolved = resolveImageSrc(
src || "",
base,
);
return (
<img
src={resolved}
alt={alt}
className="max-w-full h-auto rounded-md my-4 shadow-sm border-2 border-ctp-text"
loading="lazy"
/>
);
},
}}
components={markdownComponents}
>
{displayContent}
</ReactMarkdown>
@@ -0,0 +1,20 @@
import type { ReactElement, ReactNode } from "react";
interface PortfolioSectionProps {
title: string;
children: ReactNode;
}
export default function PortfolioSection({
title,
children,
}: PortfolioSectionProps): ReactElement {
return (
<section className="mb-10">
<h2 className="text-2xl font-bold text-ctp-lavender border-b border-ctp-green pb-2 mb-4">
{title}
</h2>
{children}
</section>
);
}
@@ -0,0 +1,13 @@
import type { ReactElement } from "react";
interface SkillsProps {
label: string;
}
export default function Skills({ label }: SkillsProps): ReactElement {
return (
<span className="border border-ctp-text rounded-full px-3 py-1 text-sm text-ctp-text transition-colors duration-300 hover:border-ctp-green hover:text-ctp-green">
{label}
</span>
);
}
@@ -0,0 +1,59 @@
import type { ReactElement } from "react";
interface TimelineEntryProps {
title: string;
subtitle: string;
subtitleUrl?: string;
date: string;
current?: boolean;
highlights: string[];
}
export default function TimelineEntry({
title,
subtitle,
subtitleUrl,
date,
current,
highlights,
}: TimelineEntryProps): ReactElement {
return (
<div className="relative pl-6 pb-8 border-l-2 border-ctp-overlay0">
<span className="absolute -left-1.25 top-1 size-2 rounded-full bg-ctp-green" />
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<h3 className="text-lg font-semibold text-ctp-text">
{title}
{current && (
<span className="ml-2 text-xs font-normal text-ctp-green border border-ctp-green rounded-full px-2 py-0.5 align-middle">
Current
</span>
)}
</h3>
<span className="text-sm text-ctp-subtext0">{date}</span>
</div>
<p className="text-ctp-mauve">
{subtitleUrl ? (
<a
href={subtitleUrl}
target="_blank"
rel="noreferrer"
className="hover:underline"
>
{subtitle}
</a>
) : (
subtitle
)}
</p>
{highlights.length > 0 && (
<ul className="list-disc pl-6 mt-2 space-y-1 text-ctp-text">
{highlights.map((h) => (
<li key={h} className="text-sm">
{h}
</li>
))}
</ul>
)}
</div>
);
}
+6 -12
View File
@@ -2,6 +2,8 @@
Currently a software engineering student. Have primarily done full-stack web development, but my interest has grown toward backend services using low-level programming languages. I am still learning as I go.
> I request that none of my projects gets hosted on platforms that are not open-source. This includes GitHub and GitLab, which has a proprietary software "Enterprise" edition. Otherwise, my projects are generally licensed as BSD 3-Clause License.
## Motivation
I value security, privacy, human health, and combating climate change. Technology can achieve all of this today to some extent. Unfortunately, we are in times of misinformation, Big Tech taking over, and science denial. In a world becoming seemingly more complex, people are overwhelmed and feel like they have no energy left to assess decisions. People do not hesitate to purchase the newest iPhone, without thinking about whether they truly need the newest device. Or they will install whatever application without thinking about possible consequences: what information is shared and sold about them, or they walk right into getting hacked and suffer financial losses and stress. Large corporations take advantage of people who do not understand such concepts; it has become all about consuming the next new thing to the detriment of people.
@@ -10,7 +12,7 @@ I wish to educate myself in technology primarily to be able to combat anti-intel
## Interests
My interests are primarily projects aiming to solve emerging problems, or ones that significantly improve development experience. I believe that software should be free and open-source.
My interests are primarily projects aiming to solve emerging problems, or ones that significantly improve development experience. I believe that software should be free and open-source (FOSS).
### Projects
@@ -25,7 +27,7 @@ A few notable projects I am interested in:
## Contact
You can contact me via **Email: aramjonghu@tutamail.com** and on **XMPP: aramjonghu@aramjonghu.dev**.
You can contact me via **XMPP: aramjonghu@aramjonghu.dev** and on **Email: aramjonghu@tutamail.com**. More info at [contact](/contact) page.
## Notable Questions
@@ -45,23 +47,15 @@ You can contact me via **Email: aramjonghu@tutamail.com** and on **XMPP: aramjon
>
> The people behind SystemD seem passionate and oppose dystopian laws. I hope that when laws cross the line, real opposition will occur.
> **Q:** What languages do you actually use?
>
> **A:** Mainly use JavaScript/TypeScript (React) and Java as this is what I learned at school. Not a fan of either language.
> **Q:** What languages are you learning?
>
> **A:** Currently in the middle of learning Rust and Zig. I especially like the direction of Zig, but due to higher demand for Rust, I will be focusing on Rust first. I have not written any Rust yet. I have written a small Zig program, however.
> **Q:** Are you on LinkedIn?
>
> **A:** No. I quit in 2025, understanding that LinkedIn has been a platform to curate a fake image in a race for relevance. It is also owned by Microsoft, which is another deterrent. In my eyes, it is slop.
> **A:** No. I quit in 2025, understanding that LinkedIn has been a platform to curate a fake image in a race for relevance. It is also owned by Microsoft, which is another deterrent. In my eyes, it is slop. Furthermore, most of these social platforms are made to spy and track people to make money rather than connect people.
> **Q:** Why do you have no personal projects on GitHub?
>
> **A:** I dropped GitHub because of concerns that my projects would be trained on AI. On top of that, the platform's uptime is becoming increasingly spotty. There have been a few incidents such as projects losing critical progress due to data corruption in pull requests. Worst of all, banning developers who offer critical contributions from GitHub (and sometimes also on GitLab).
>
> Although my projects are not anything special today, in preparation, I host my own instance of Forgejo. I also wish that none of my projects be used or shared on proprietary platforms. They do not respect developers, so we should not respect their service either. For work, I will have to use GitHub, which is the only use case GitHub will have for me.
> Although my projects are not anything special today, in preparation, I host my own instance of Forgejo. I also wish that none of my projects be used or shared on proprietary platforms. They do not respect developers, so we should not respect their service either. For work, I will have to use GitHub, which is the only use case GitHub will have for me. You can find me if you are interested.
> **Q:** Do you support AI?
>
+50
View File
@@ -0,0 +1,50 @@
Anyone is free to contact me for any reason. Below are ways to contact me.
## XMPP
You can reach me at **aramjonghu@aramjonghu.dev**.
My preferred way of contacting is via a [XMPP](https://xmpp.org/) client. It is a privacy focused standard and it is decentralized. No one owns XMPP. Look [here](https://xmpp.org/software/?category=clients) to determine which client options there are for your operating system (or browser). But generally:
| Platform | Client | Offers server for login |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| Linux | Dino | No |
| Windows | Converse.js (browser based), but I would recommend Movim in a browser. It seems that there is no solid option that supports calls on Windows. | Yes for both |
| Android/AOSP | Conversations (free in fdroid store) or Cheogram | Conversations has, Cheogram is a fork of Conversations |
| iOS | Monal IM or Siskin IM | Siskin does via tigase.im |
| MacOS | Monal IM or Converse.js for browser based | Conversejs does |
| Browser | Movim or Converse.js | Yes for both |
Note that you can use any client even if they do not offer their own server for account creation. You can create an account on any provider such as conversejs.org. Then log in to your preferred client.
## Email
Not preferred, but you can contact me via email at **aramjonghu@tutamail.com**.
## Other platforms
For various reasons, I am not on a handful of the larger platforms. Check on the [home](/) page in the **About** section under the **Notable Questions** header if you wish to know why.
> Below I drop a sort of rant on secure messaging, you can skip reading. I will move this elsewhere later.
### Not recommended
I do not use various chat apps, or I do not recommend them for private messaging. Some examples:
- SMS
- WhatsApp (anything from Meta)
- Telegram
- Line (both JP and World versions)
- Viber
These options lack privacy and sometimes basic security features. Metadata is often enough not end-to-end encrypted, let alone the messages themselves. Metadata is generally enough information to infer information about you that should have stayed between you and the recipient. Data from these apps are used to track you, share your information for various reasons such as advertising or AI training among other reasons detrimental to the user.
### Platforms I'd recommend
There are a few platforms I generally would recommend, though if I am on them, my contact details are not shared publicly.
- Signal
- SimpleX
- Briar
Each is solid in their own right. Signal is probably the easiest for users to switch from if they are used to apps like Line or WhatsApp. One downside is that Signal still requires a phone number, which can be personally identifiable.
+1 -1
View File
@@ -4,4 +4,4 @@ Currently learning Rust and Zig programming languages.
### Projects
Currently no projects planned.
MarAn is a webpage I am working on for a freelancer.
+100
View File
@@ -0,0 +1,100 @@
{
"profile": {
"name": "AramJonghu",
"headline": "Software Engineer",
"region": "",
"location": "The Netherlands",
"summary": "I am third year software engineering student. I have primarily done full-stack web development, but my interest has grown towards backend services. Languages like Zig interest me with their vision on how code can be written.\n\nRead the about section on the home page. I write about my motivation, interest, how to contact me and I answer some notable questions.\n",
"links": [
{
"label": "Forgejo",
"svg": "forgejo",
"url": "https://git.aramjonghu.dev/AramJonghu"
},
{
"label": "Gitea",
"svg": "gitea",
"url": "https://git.zach-dev.cc/AramJonghu"
}
]
},
"experience": [
{
"role": "Backend Developer",
"company": "HANflex",
"companyUrl": "https://www.han.nl/over-de-han/werken-bij-de-han/andere-inzetvormen-bij-de-han/",
"start": "2026-03",
"end": "present",
"current": true,
"highlights": [
"Learnt basic python and typedb",
"Learning Rust as potential replacement of some python codebases"
]
}
],
"education": [
{
"degree": "IT - Software Engineer (Bachelor)",
"school": "HAN University of Applied Sciences",
"schoolUrl": "https://han.nl",
"start": "2023",
"end": "expected 2028",
"current": true,
"highlights": [
"Java OOP",
"PHP Security",
"JavaScript/React Frontend Webdev",
"Java Backend Webdev"
]
}
],
"skills": [
{
"category": "Languages",
"items": ["JavaScript", "TypeScript", "Java"]
},
{
"category": "Tools",
"items": [
"Linux",
"Git",
"React",
"Neovim",
"Docker",
"CI/CD",
"Nginx"
]
},
{ "category": "Learning", "items": ["Zig", "Rust", "Jujutsu"] }
],
"languages": [
{ "name": "English", "proficiency": "Fluent" },
{ "name": "Dutch", "proficiency": "Native" },
{ "name": "Armenian", "proficiency": "Basic conversational" }
],
"projects": [
{ "name": "HANflex kennisplatform",
"url": "https://github.com/AIM-kennisplatformen",
"description": "Repositories I contributed to. Mainly studio and database-builder-scepa.",
"tags": ["Python", "TypeDB"]
},
{
"name": "aramjonghu-site",
"url": "https://git.aramjonghu.dev/AramJonghu/aramjonghu-site",
"description": "Personal website and central hub for my projects, systems, and self-hosted services.",
"tags": ["React", "TypeScript"]
},
{
"name": "MarAn-site",
"url": "https://git.aramjonghu.dev/AramJonghu/maran-site",
"description": "Portfolio site with Zig backend serving static frontend + contact API.",
"tags": ["Zig using zap", "JavaScript", "plain HTML"]
},
{
"name": "z-bar-qt (also known as ZShell)",
"url": "https://git.zach-dev.cc/zach/z-bar-qt",
"description": "ZShell is a Hyprland-focused desktop shell built with Quickshell + Qt6/QML.\nI mostly contributed by adding CI/CD and Python",
"tags": ["QML", "Python", "C++", "Rust"]
}
]
}
+17
View File
@@ -0,0 +1,17 @@
import { type ReactElement } from "react";
import MdProcessor from "../components/MdProcessor";
import contact from "../content/contact.md?raw";
export default function CurrentActivity(): ReactElement {
return (
<div className="text-ctp-text flex flex-col min-h-screen px-4 py-6">
<div className="w-full flex flex-col flex-1">
<h1 className="text-4xl text-center font-bold text-ctp-mauve mb-6">
Contact
</h1>
<MdProcessor content={contact} />
</div>
</div>
);
}
+4 -64
View File
@@ -1,14 +1,7 @@
import { useState, type ReactElement } from "react";
import MdProcessor from "../components/MdProcessor";
import CurrentActivity from "../components/CurrentActivity";
import About from "../components/About";
interface ProjectInfo {
id: string;
name: string;
url: string;
}
interface Section {
id: string;
title: string;
@@ -16,59 +9,15 @@ interface Section {
}
export default function Home(): ReactElement {
const projectList: ProjectInfo[] = [
{
id: "site",
name: "aramjonghu-site",
url: "https://git.aramjonghu.dev/AramJonghu/aramjonghu-site/raw/branch/master/README.md",
},
{
id: "file-organizer",
name: "file-organizer written in zig",
url: "https://git.aramjonghu.dev/AramJonghu/file-organizer/src/branch/main/README.md",
},
{
id: "nvim",
name: "Nvim Config",
url: "https://git.aramjonghu.dev/AramJonghu/nvim/src/branch/main/README.md",
},
];
const [projIdx, setProjIdx] = useState<number>(0);
const prevProject = () =>
setProjIdx((i) => (i - 1 + projectList.length) % projectList.length);
const nextProject = () => setProjIdx((i) => (i + 1) % projectList.length);
const [activeTab, setActiveTab] = useState<string>("projects");
const [activeTab, setActiveTab] = useState<string>("about");
const sections: Section[] = [
{
id: "projects",
title: "Projects",
id: "about",
title: "About",
content: (
<>
<h1 className="text-center m-6 text-3xl font-bold text-ctp-mauve">
Projects
</h1>
<div className="flex justify-center mb-6">
<button
onClick={prevProject}
className="px-3 py-1 rounded ui-btn text-ctp-text"
>
Prev
</button>
<span className="self-center mx-6 font-medium">
{projectList[projIdx].name} ({projIdx + 1}/
{projectList.length})
</span>
<button
onClick={nextProject}
className="px-3 py-1 rounded ui-btn text-ctp-text"
>
Next
</button>
</div>
<MdProcessor repoUrl={projectList[projIdx].url} />
<About />
</>
),
},
@@ -81,15 +30,6 @@ export default function Home(): ReactElement {
</>
),
},
{
id: "about",
title: "About",
content: (
<>
<About />
</>
),
},
];
return (
+247
View File
@@ -0,0 +1,247 @@
import type { ReactElement } from "react";
import PortfolioSection from "../components/portfolio/PortfolioSection";
import TimelineEntry from "../components/portfolio/TimelineEntry";
import Skills from "../components/portfolio/Skills";
import portfolio from "../content/portfolio.json";
import forgejo from "../assets/images/forgejo.svg";
import gitea from "../assets/images/gitea.svg";
import avatar from "../assets/images/portfolio_avatar.png";
const LOGOS: Record<string, string> = { forgejo, gitea };
interface Profile {
name: string;
headline: string;
region: string;
location: string;
links: { label: string; svg: string; url: string }[];
summary: string;
}
interface TimelineItem {
role?: string;
company?: string;
companyUrl?: string;
degree?: string;
school?: string;
schoolUrl?: string;
start: string;
end: string;
current: boolean;
highlights: string[];
}
interface Experience extends TimelineItem {
role: string;
company: string;
companyUrl?: string;
}
interface Education extends TimelineItem {
degree: string;
school: string;
schoolUrl?: string;
}
interface SkillGroup {
category: string;
items: string[];
}
interface Language {
name: string;
proficiency: string;
}
interface Project {
name: string;
url: string;
description: string;
tags: string[];
}
interface PortfolioData {
profile: Profile;
experience: Experience[];
education: Education[];
skills: SkillGroup[];
languages: Language[];
projects: Project[];
}
const data = portfolio as PortfolioData;
const MONTHS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const formatDate = (value: string): string => {
if (value.toLowerCase() === "present") return "Present";
const [year, month] = value.split("-");
return month ? `${MONTHS[Number(month) - 1]} ${year}` : year;
};
const formatRange = (start: string, end: string): string =>
`${formatDate(start)} ${formatDate(end)}`;
export default function Portfolio(): ReactElement {
const { profile, experience, education, skills, languages, projects } =
data;
return (
<div className="text-ctp-text flex flex-col min-h-screen px-4 py-6">
<div className="max-w-340 min-w-96 mx-auto flex flex-col flex-1">
<h1 className="text-4xl text-center font-bold text-ctp-mauve mb-6">
Portfolio
</h1>
<div className="grid md:grid-cols-[10fr_6fr] gap-8 items-start">
<div>
<PortfolioSection title="Projects & Contributions">
{projects.map((proj) => (
<a
key={proj.name}
href={proj.url}
target="_blank"
rel="noreferrer"
className="block bg-ctp-mantle border border-ctp-lavender-800 rounded-xl p-4 mb-4 hover:border-ctp-green transition-colors"
>
<h3 className="font-semibold text-ctp-mauve hover:underline">
{proj.name}
</h3>
<p className="text-sm text-ctp-text mt-1 whitespace-pre-line">
{proj.description}
</p>
<div className="flex flex-wrap gap-2 mt-3">
{proj.tags.map((tag) => (
<Skills key={tag} label={tag} />
))}
</div>
</a>
))}
</PortfolioSection>
<PortfolioSection title="Skills">
{skills.map((group) => (
<div key={group.category} className="mb-4">
<h3 className="text-ctp-lavender font-semibold mb-2">
{group.category}
</h3>
<div className="flex flex-wrap gap-2">
{group.items.map((skill) => (
<Skills key={skill} label={skill} />
))}
</div>
</div>
))}
</PortfolioSection>
<PortfolioSection title="Education">
{education.map((edu) => (
<TimelineEntry
key={edu.degree}
title={edu.degree}
subtitle={edu.school}
subtitleUrl={edu.schoolUrl}
date={formatRange(edu.start, edu.end)}
current={edu.current}
highlights={edu.highlights}
/>
))}
</PortfolioSection>
<PortfolioSection title="Experience">
{experience.map((exp) => (
<TimelineEntry
key={exp.role}
title={exp.role}
subtitle={exp.company}
subtitleUrl={exp.companyUrl}
date={formatRange(exp.start, exp.end)}
current={exp.current}
highlights={exp.highlights}
/>
))}
</PortfolioSection>
</div>
<div className="space-y-6 order-first md:order-0">
<div className="bg-ctp-mantle border border-ctp-lavender-800 rounded-xl p-6">
<img
src={avatar}
alt={`${profile.name} avatar`}
className="size-28 border border-ctp-lavender-800 rounded-xl object-cover mb-4"
/>
<h2 className="text-3xl font-bold text-ctp-mauve">
{profile.name}
</h2>
<p className="text-ctp-green mt-1">
{profile.headline}
</p>
<p className="text-sm text-ctp-subtext0 mt-3">
{profile.location}
</p>
<p className="text-sm text-ctp-subtext0">
{profile.region}
</p>
<div className="flex flex-wrap gap-2 mt-4">
{profile.links.map((link) => (
<a
key={link.label}
href={link.url}
target="_blank"
rel="noreferrer"
aria-label={link.label}
title={link.label}
className="p-2 cursor-pointer hover:opacity-80 transition-opacity"
>
<img
src={LOGOS[link.svg]}
alt={link.label}
className={`w-7 h-7 ${
link.svg === "github"
? "icon-invert"
: ""
}`}
/>
</a>
))}
</div>
</div>
<PortfolioSection title="Summary">
<p className="text-sm leading-relaxed whitespace-pre-line">
{profile.summary}
</p>
</PortfolioSection>
<PortfolioSection title="Languages">
<ul className="space-y-2">
{languages.map((lang) => (
<li
key={lang.name}
className="flex justify-between text-sm border-b border-ctp-surface0 pb-2"
>
<span>{lang.name}</span>
<span className="text-ctp-subtext0">
{lang.proficiency}
</span>
</li>
))}
</ul>
</PortfolioSection>
</div>
</div>
</div>
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { createHighlighter, type Highlighter } from "shiki";
const highlighterPromise = createHighlighter({
themes: ["catppuccin-macchiato", "catppuccin-latte"],
langs: [
"javascript",
"typescript",
"jsx",
"tsx",
"css",
"html",
"json",
"bash",
"shellscript",
"python",
"markdown",
"yaml",
"toml",
"rust",
"sql",
"diff",
"docker",
"go",
"java",
"kotlin",
"swift",
"c",
"cpp",
"odin",
"csharp",
"php",
"ruby",
"lua",
"scss",
"vue",
"svelte",
"nix",
"zig",
"makefile",
"cmake",
"powershell",
"nginx",
"latex",
"tex",
],
});
export function getHighlighter(): Promise<Highlighter> {
return highlighterPromise;
}