Compare commits

..
5 Commits
Author SHA1 Message Date
AramJonghu 5fcae9d1fb Removal of jj commit contents, test commits 2026-07-02 23:41:41 +02:00
AramJonghu 5930df4f4f Removing test commit changes with jj 2026-07-02 23:38:16 +02:00
AramJonghu 039568f1ec a commit after jj new to test if I do correctly 2026-07-02 19:51:21 +02:00
AramJonghu 1840fbe4be another addition test 2026-07-02 19:49:00 +02:00
AramJonghu 207672c7dd testing addition 2026-07-02 19:48:13 +02:00
37 changed files with 491 additions and 1426 deletions
-37
View File
@@ -1,37 +0,0 @@
name: Format, lint, typecheck & build
on:
push:
branches: ["master"]
jobs:
check:
runs-on: alpine
container: node:lts-alpine
steps:
- name: Install dependencies
run: apk add --no-cache git
- name: Checkout repo
uses: actions/checkout@v4
- name: Install frontend deps
working-directory: frontend
run: npm install
- name: Run eslint check
working-directory: frontend
run: npm run lint
- name: Run typecheck
working-directory: frontend
run: npm run typecheck
- name: Run Prettier check
working-directory: frontend
run: npx prettier --check .
- name: Run build check
working-directory: frontend
run: npm run build
+25
View File
@@ -0,0 +1,25 @@
name: Format frontend with Prettier
on:
push:
branches: ["master"]
jobs:
format:
runs-on: alpine
container: node:26-alpine
steps:
- name: Install dependencies
run: apk add --no-cache git
- name: Checkout repo
uses: actions/checkout@v4
- name: Install frontend deps
working-directory: frontend
run: npm install
- name: Run Prettier check
working-directory: frontend
run: npx prettier --check .
-29
View File
@@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2025, AramJonghu
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+29 -38
View File
@@ -8,38 +8,35 @@ This site is not just a portfolio — it acts as an entry point into a larger ec
## Overview ## Overview
The site is divided into two conceptual layers: The site is divided into three conceptual layers:
### 1. Tabs (Home) ### 1. Home
The homepage provides tabs for three sections: 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.
- **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 ### 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 - Nextcloud — file storage and synchronization
- Navidrome — personal music streaming - Navidrome — personal music streaming
- Forgejo Git — Git hosting and development platform - Forgejo — Git hosting and development platform
- Stream — live media platform powered by MediaMTX (internal route) - OmniSearch — privacy-focused search engine
- Omnisearch — privacy-focused search engine - Stream service — live media platform powered by MediaMTX
- Gitea zach-dev — external development collaboration - Friends Gitea instance — external development collaboration
## Stack These services are part of my personal infrastructure and exist outside the portfolio itself.
| Layer | Choice | Notes | ### 3. Pages
| --------- | ---------------------------- | ----------------------------------------- |
| Language | TypeScript / React 19 | Vite 8 + rolldown | Dedicated pages for deeper content:
| CSS | TailwindCSS v4 | Catppuccin macchiato/latte |
| Markdown | react-markdown v10 | remark-gfm, rehype-raw, rehype-sanitize | - Projects — selected work and builds
| Syntax HL | Shiki v4 | Catppuccin themes, CSS variable switching | - Systems — architecture and self-hosted infrastructure overview
| Font | Inter (self-hosted, 4 WOFF2) | no Google Fonts | - About — personal background and context
| Theme | Catppuccin macchiato/latte | JS toggle, persisted to localStorage | - CV — formal resume (if applicable)
| Git host | Forgejo (self-hosted) | git.aramjonghu.dev |
## Philosophy ## Philosophy
@@ -54,22 +51,16 @@ Instead of a traditional portfolio, it focuses on:
It evolves alongside my personal development and technical interests. It evolves alongside my personal development and technical interests.
## Project layout ## Stack
``` - React
├── public/ favicon - Vite
├── src/ - Tailwind CSS
│ ├── assets/ images, fonts, logo - Catppuccin theme
│ ├── components/ MdProcessor, Header, Layout, ProjectInfo, ... - Self-hosted backend services (Forgejo, Nextcloud, etc.)
│ ├── content/ about.md, current-activity.md, projects.json
│ ├── hooks/ useTheme
│ ├── pages/ Home
│ └── utils/ theme, highlighter
├── index.html
├── package.json
└── LICENSE
```
## License ## Status
BSD 3-Clause. See [LICENSE](https://git.aramjonghu.dev/AramJonghu/aramjonghu-site/src/branch/master/LICENSE). This site is continuously evolving and serves as both a portfolio and experimental environment.
Expect structural changes as systems grow.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 177 KiB

+8 -9
View File
@@ -11,7 +11,6 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@shikijs/rehype": "^4.3.1",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
@@ -19,26 +18,26 @@
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"shiki": "^4.3.1", "tailwindcss": "^4.2.2"
"tailwindcss": "^4.2.2",
"typescript-eslint": "^8.67.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^8.0.1", "@babel/core": "^7.29.0",
"@catppuccin/tailwindcss": "^1.0.0", "@catppuccin/tailwindcss": "^1.0.0",
"@eslint/js": "^10.0.1", "@eslint/js": "^9.39.4",
"@rolldown/plugin-babel": "^0.2.2", "@rolldown/plugin-babel": "^0.2.2",
"@tailwindcss/language-server": "^0.16.0", "@tailwindcss/language-server": "^0.14.29",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.8.0", "eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0", "globals": "^17.4.0",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"typescript": "^7.0.2", "typescript": "^5.7.3",
"typescript-eslint": "^8.61.1",
"typescript-language-server": "^5.3.0",
"vite": "^8.0.4", "vite": "^8.0.4",
"vscode-langservers-extracted": "^4.10.0" "vscode-langservers-extracted": "^4.10.0"
}, },
+1 -63
View File
@@ -1,54 +1,8 @@
@import "tailwindcss"; @import "tailwindcss";
@import "@catppuccin/tailwindcss/macchiato.css"; @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 { .ui-btn {
@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; @apply text-ctp-text border border-ctp-text rounded px-3 py-1 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 { .ui-btn:hover {
@@ -68,19 +22,3 @@
.stream-frame iframe { .stream-frame iframe {
@apply w-full h-full border-0; @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);
}
+3 -11
View File
@@ -4,10 +4,8 @@ import "./App.css";
import Layout from "./components/Layout"; import Layout from "./components/Layout";
import Home from "./pages/Home"; import Home from "./pages/Home";
import Stream from "./pages/Stream"; import Stream from "./pages/Stream";
import Contact from "./pages/Contact";
import Portfolio from "./pages/Portfolio";
export default function App(): ReactElement { function App(): ReactElement {
const path = window.location.pathname.toLowerCase(); const path = window.location.pathname.toLowerCase();
let content: ReactElement = <Home />; let content: ReactElement = <Home />;
@@ -15,13 +13,7 @@ export default function App(): ReactElement {
content = <Stream />; content = <Stream />;
} }
if (path === "/contact") {
content = <Contact />;
}
if (path === "/portfolio") {
content = <Portfolio />;
}
return <Layout>{content}</Layout>; 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
@@ -1,19 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-38
View File
@@ -1,38 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-5
View File
@@ -1,5 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

-8
View File
@@ -1,8 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

+10 -6
View File
@@ -1,11 +1,15 @@
import { type ReactElement } from "react"; export default function About() {
import MdProcessor from "./MdProcessor";
import aboutMd from "../content/about.md?raw";
export default function About(): ReactElement {
return ( return (
<> <>
<MdProcessor content={aboutMd} /> <h1 className="text-center m-6 text-3xl font-bold text-ctp-mauve">
About
</h1>
<div className="flex justify-center mb-6">
<p className="text-ctp-text">
</p>
<p className="text-ctp-text"></p>
</div>
</> </>
); );
} }
@@ -1,11 +0,0 @@
import { type ReactElement } from "react";
import MdProcessor from "./MdProcessor";
import currentActivityMd from "../content/current-activity.md?raw";
export default function CurrentActivity(): ReactElement {
return (
<>
<MdProcessor content={currentActivityMd} />
</>
);
}
+31 -60
View File
@@ -1,71 +1,42 @@
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 forgejo from "../assets/images/forgejo.svg";
import gitea from "../assets/images/gitea.svg"; import gitea from "../assets/images/gitea.svg";
import search from "../assets/images/search.svg";
interface IconLink { export default function Footer() {
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 currentYear = new Date().getFullYear();
const startYear = 2025;
const goToForgejo = () => {
window.location.href =
"https://git.aramjonghu.nl/AramJonghu/aramjonghu-site";
};
const goToGitea = () => {
window.location.href =
"https://git.zach-dev.cc/AramJonghu/aramjonghu-site";
};
return ( return (
<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"> <footer className="bg-ctp-mantle flex items-center justify-between px-24 py-6 border-t border-ctp-lavender-800 mt-10">
<p className="text-sm text-ctp-text"> <p className="text-sm text-ctp-text">
&copy; {startYear} - {currentYear} AramJonghu. All rights &copy; {currentYear} AramJonghu. All rights reserved.
reserved.
</p> </p>
<div className="flex items-center gap-2"> <div className="flex">
{iconLinks.map((icon) => ( <p className="text-sm text-ctp-text">
<a Check out the website repository here:
key={icon.label} </p>
href={icon.url}
target="_blank" <img
rel="noreferrer" src={forgejo}
aria-label={icon.label} alt="Forgejo repository"
title={icon.label} onClick={goToForgejo}
className="p-2 cursor-pointer hover:opacity-80 transition-opacity" className="ml-2 w-6 h-6 cursor-pointer hover:opacity-80 transition-opacity"
> />
<img
src={icon.src} <img
alt={icon.label} src={gitea}
className={`w-7 h-7 ${icon.invert ? "icon-invert" : ""}`} alt="Gitea mirror"
/> onClick={goToGitea}
</a> className="ml-2 w-6 h-6 cursor-pointer hover:opacity-80 transition-opacity"
))} />
</div> </div>
</footer> </footer>
); );
+23 -77
View File
@@ -1,7 +1,6 @@
import { useState } from "react";
import Aku from "../assets/images/aku.png"; import Aku from "../assets/images/aku.png";
interface PageItem { interface NavItem {
label: string; label: string;
url: string; url: string;
} }
@@ -17,99 +16,46 @@ interface HeaderProps {
export default function Header({ theme }: HeaderProps) { export default function Header({ theme }: HeaderProps) {
const { theme: currentTheme, toggleTheme } = theme; const { theme: currentTheme, toggleTheme } = theme;
const [menuOpen, setMenuOpen] = useState(false);
const pageItems: PageItem[] = [ const navItems: NavItem[] = [
{ label: "Nextcloud", url: "https://nextcloud.aramjonghu.nl" },
{ label: "Navidrome", url: "https://music.aramjonghu.nl" },
{ label: "Forgejo Git", url: "https://git.aramjonghu.nl" },
{ label: "Stream", url: "/stream" }, { label: "Stream", url: "/stream" },
{ label: "Portfolio", url: "/portfolio" }, { label: "Omnisearch", url: "https://xng.aramjonghu.nl" },
{ label: "Contact", url: "/contact" }, { label: "Gitea zach-dev", url: "https://git.zach-dev.cc" },
]; ];
const goHome = () => { const goHome = () => {
window.location.href = "/"; window.location.href = "/";
}; };
const closeMenu = () => {
setMenuOpen(false);
};
return ( 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"> <header className="bg-ctp-mantle flex justify-between items-center px-12 py-6 gap-6 border-b border-ctp-lavender-800">
<div className="grid grid-cols-3 items-center w-full md:hidden gap-4"> <img
<img className="size-16 cursor-pointer"
className="size-16 cursor-pointer justify-self-start" src={Aku}
src={Aku} alt="Home"
alt="Home" onClick={goHome}
onClick={goHome} />
/>
<button <nav className="flex flex-wrap gap-3 items-center">
className="theme-ui-btn ui-btn:hover font-[NerdFontSymbols] justify-self-center" {navItems.map((item) => (
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 <a
key={page.label} key={item.label}
href={page.url} href={item.url}
className="ui-btn ui-btn:hover px-2 py-1 md:px-3 md:py-1" className="ui-btn ui-btn:hover"
> >
{page.label} {item.label}
</a> </a>
))} ))}
</nav> </nav>
<div className="hidden md:block"> <div>
<button <button className="ui-btn ui-btn:hover" onClick={toggleTheme}>
className="theme-ui-btn ui-btn:hover text-lg font-[NerdFontSymbols]" {currentTheme === "macchiato" ? "🌙 Dark" : "☀️ Light"}
onClick={toggleTheme}
>
{currentTheme === "macchiato" ? "" : ""}
</button> </button>
</div> </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> </header>
); );
} }
-372
View File
@@ -1,372 +0,0 @@
import {
useEffect,
useState,
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;
if (
repoUrl.includes("github.com") &&
!repoUrl.includes("raw.githubusercontent.com")
) {
return repoUrl
.replace("github.com", "raw.githubusercontent.com")
.replace("/blob/", "/");
}
if (repoUrl.includes("/src/branch/")) {
return repoUrl.replace("/src/branch/", "/raw/branch/");
}
return repoUrl;
};
const getRepoRawBase = (repoUrl: string): string | null => {
const rawUrl = toRawUrl(repoUrl);
if (!rawUrl) return null;
try {
const url = new URL(rawUrl);
const parts = url.pathname.split("/").filter(Boolean);
if (url.hostname.includes("raw.githubusercontent.com")) {
return `${url.origin}/${parts.slice(0, 3).join("/")}`;
}
const rawIndex = parts.indexOf("raw");
if (rawIndex !== -1) {
return `${url.origin}/${parts.slice(0, rawIndex + 3).join("/")}`;
}
return null;
} catch {
return null;
}
};
const resolveImageSrc = (src: string, base: string | null): string => {
if (!src) return src;
if (src.startsWith("http")) return src;
if (!base) return src;
const clean = src.replace(/^.\//, "");
if (src.startsWith("/")) return base + src;
return `${base}/${clean}`;
};
interface MdProcessorProps {
content?: string;
repoUrl?: string;
}
export default function MdProcessor({
content: directContent,
repoUrl,
}: MdProcessorProps): ReactElement {
const useRemote = !directContent && !!repoUrl;
const [fetchState, setFetchState] = useState<{
url: string;
content: string;
}>({ url: "", content: "" });
useEffect(() => {
if (!useRemote || !repoUrl) return;
const fetchREADME = async (url: string): Promise<string> => {
try {
const rawUrl = toRawUrl(url);
const response = await fetch(rawUrl);
if (!response.ok) {
throw new Error("Failed to fetch README");
}
return await response.text();
} catch (error) {
console.error(error);
return "Error fetching README. Make sure the URL points to a raw markdown file.";
}
};
fetchREADME(repoUrl).then((content) => {
setFetchState({ url: repoUrl, content });
});
}, [useRemote, repoUrl]);
const base = useMemo(() => {
if (directContent) return null;
return repoUrl ? getRepoRawBase(repoUrl) : null;
}, [directContent, repoUrl]);
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 ? (
<div className="flex flex-col gap-3 animate-pulse w-full">
<div className="h-4 bg-ctp-overlay0 rounded-full w-3/4"></div>
<div className="h-4 bg-ctp-overlay0 rounded-full w-3/4"></div>
<div className="h-4 bg-ctp-overlay0 rounded-full w-3/4"></div>
</div>
) : (
<div className="text-ctp-text leading-relaxed prose prose-base md:prose-lg dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={markdownComponents}
>
{displayContent}
</ReactMarkdown>
</div>
)}
</div>
);
}
+271
View File
@@ -0,0 +1,271 @@
import { useEffect, useState, useMemo, type ReactElement } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
const toRawUrl = (repoUrl: string): string => {
if (!repoUrl) return repoUrl;
if (
repoUrl.includes("github.com") &&
!repoUrl.includes("raw.githubusercontent.com")
) {
return repoUrl
.replace("github.com", "raw.githubusercontent.com")
.replace("/blob/", "/");
}
if (repoUrl.includes("/src/branch/")) {
return repoUrl.replace("/src/branch/", "/raw/branch/");
}
return repoUrl;
};
const getRepoRawBase = (repoUrl: string): string | null => {
const rawUrl = toRawUrl(repoUrl);
if (!rawUrl) return null;
try {
const url = new URL(rawUrl);
const parts = url.pathname.split("/").filter(Boolean);
if (url.hostname.includes("raw.githubusercontent.com")) {
return `${url.origin}/${parts.slice(0, 3).join("/")}`;
}
const rawIndex = parts.indexOf("raw");
if (rawIndex !== -1) {
return `${url.origin}/${parts.slice(0, rawIndex + 3).join("/")}`;
}
return null;
} catch {
return null;
}
};
const resolveImageSrc = (src: string, base: string | null): string => {
if (!src) return src;
if (src.startsWith("http")) return src;
if (!base) return src;
const clean = src.replace(/^.\//, "");
if (src.startsWith("/")) return base + src;
return `${base}/${clean}`;
};
const fetchREADME = async (repoUrl: string): Promise<string> => {
try {
const rawUrl = toRawUrl(repoUrl);
const response = await fetch(rawUrl);
if (!response.ok) {
throw new Error("Failed to fetch README");
}
return await response.text();
} catch (error) {
console.error(error);
return "Error fetching README. Make sure the URL points to a raw markdown file.";
}
};
interface ProjectsReadmeProps {
repoUrl: string;
}
export default function ProjectsReadme({
repoUrl,
}: ProjectsReadmeProps): ReactElement {
const [readmeContent, setReadmeContent] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(true);
const base = useMemo(() => getRepoRawBase(repoUrl), [repoUrl]);
useEffect(() => {
fetchREADME(repoUrl).then((content) => {
setReadmeContent(content);
setIsLoading(false);
});
}, [repoUrl]);
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">
{isLoading ? (
<div className="flex flex-col gap-3 animate-pulse w-full">
<div className="h-4 bg-ctp-overlay0 rounded-full w-3/4"></div>
<div className="h-4 bg-ctp-overlay0 rounded-full w-3/4"></div>
<div className="h-4 bg-ctp-overlay0 rounded-full w-3/4"></div>
</div>
) : (
<div className="text-ctp-text leading-relaxed prose prose-base md:prose-lg dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={{
code({
className,
children,
...props
}: React.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
}: React.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,
}: React.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,
}: React.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,
}: React.ComponentPropsWithoutRef<"h3">): ReactElement {
return (
<h3 className="text-xl font-bold text-ctp-lavender mt-5 mb-3">
{children}
</h3>
);
},
p({
children,
}: React.ComponentPropsWithoutRef<"p">): ReactElement {
return (
<p className="mb-4 text-ctp-text">
{children}
</p>
);
},
ul({
children,
}: React.ComponentPropsWithoutRef<"ul">): ReactElement {
return (
<ul className="list-disc pl-6 mb-4">
{children}
</ul>
);
},
ol({
children,
}: React.ComponentPropsWithoutRef<"ol">): ReactElement {
return (
<ol className="list-decimal pl-6 mb-4">
{children}
</ol>
);
},
li({
children,
}: React.ComponentPropsWithoutRef<"li">): ReactElement {
return <li className="mb-1">{children}</li>;
},
a({
children,
href,
}: React.ComponentPropsWithoutRef<"a">): ReactElement {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="hover:underline text-ctp-green"
>
{children}
</a>
);
},
blockquote({
children,
}: React.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>
);
},
img({
src,
alt,
}: React.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"
/>
);
},
}}
>
{readmeContent}
</ReactMarkdown>
</div>
)}
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
export default function Systems() {
return (
<>
<h1 className="text-center m-6 text-3xl font-bold text-ctp-mauve">
Systems
</h1>
<div></div>
</>
);
}
@@ -1,20 +0,0 @@
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>
);
}
@@ -1,13 +0,0 @@
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>
);
}
@@ -1,59 +0,0 @@
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>
);
}
-68
View File
@@ -1,68 +0,0 @@
## Background
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.
I wish to educate myself in technology primarily to be able to combat anti-intellectualism and offer free software to improve people's lives respecting human rights.
## 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 (FOSS).
### Projects
A few notable projects I am interested in:
- GrapheneOS
- Zig
- Hyprland
- Linux
- Osu!
- Jujutsu
## Contact
You can contact me via **XMPP: aramjonghu@aramjonghu.dev** and on **Email: aramjonghu@tutamail.com**. More info at [contact](/contact) page.
## Notable Questions
**Note that I am fully open to being wrong on some of the claims below.**
> **Q:** Why "AramJonghu"?
>
> **A:** I played on a Minecraft server with friends and friends of friends back in the 1.5 days. One of them would often say or type "Aram Jonguh" — Jonguh being akin to "bro" in Dutch, so it says "Aram Bro". Liking the name, my IGN became AramJonghu. It is spelled incorrectly, which I did not correct at the time. It has persisted ever since.
> **Q:** What is my setup?
>
> **A:** I currently use Artix with OpenRC on Hyprland. I use neovim in the Ghostty terminal (with tmux). My server runs Alpine with OpenRC.
> **Q:** Are you specifically avoiding SystemD?
>
> **A:** Yes, but it is not due to the infamous pull request. The concern is not about systemd applying age verification (they have not). Rather, I want to explore other options in case SystemD becomes compromised, so to speak. My main reason for switching is to have an init system be an init system first and foremost. A smaller codebase makes it easier to manage.
>
> The people behind SystemD seem passionate and oppose dystopian laws. I hope that when laws cross the line, real opposition will occur.
> **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. 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. You can find me if you are interested.
> **Q:** Do you support AI?
>
> **A:** Generally, no. I believe AI (LLMs) has done more harm than good. From data collection, climate harm, theft of data and art, and the damage it has done to people who misuse it to skip proper learning, and the billion-dollar circlejerk investments that are actively killing personal computing. The technology itself is interesting and I see real value for specific use cases such as in the health sector to better diagnose patients, improve translation accuracy, or improve development experience. This should not, however, outweigh the many downsides.
>
> I try to use AI as sparingly as possible, but I also catch myself being reliant more often than not on some days. In the end, AI should be used as a tool to assist us.
> **Q:** Are billionaires evil?
>
> **A:** Yes.
-50
View File
@@ -1,50 +0,0 @@
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.
-7
View File
@@ -1,7 +0,0 @@
### Language learning
Currently learning Rust and Zig programming languages.
### Projects
MarAn is a webpage I am working on for a freelancer.
-100
View File
@@ -1,100 +0,0 @@
{
"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
@@ -1,17 +0,0 @@
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>
);
}
+79 -11
View File
@@ -1,7 +1,14 @@
import { useState, type ReactElement } from "react"; import { useState, type ReactElement } from "react";
import CurrentActivity from "../components/CurrentActivity"; import ProjectsReadme from "../components/ProjectsReadme";
import Systems from "../components/Systems";
import About from "../components/About"; import About from "../components/About";
interface ProjectInfo {
id: string;
name: string;
url: string;
}
interface Section { interface Section {
id: string; id: string;
title: string; title: string;
@@ -9,9 +16,71 @@ interface Section {
} }
export default function Home(): ReactElement { export default function Home(): ReactElement {
const [activeTab, setActiveTab] = useState<string>("about"); const projectList: ProjectInfo[] = [
{
id: "site",
name: "aramjonghu-site",
url: "https://git.aramjonghu.nl/AramJonghu/aramjonghu-site/raw/branch/master/README.md",
},
{
id: "file-organizer",
name: "file-organizer written in zig",
url: "https://git.aramjonghu.nl/AramJonghu/file-organizer/src/branch/main/README.md",
},
{
id: "nvim",
name: "Nvim Config",
url: "https://git.aramjonghu.nl/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 sections: Section[] = [ const sections: Section[] = [
{
id: "projects",
title: "Projects",
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>
<ProjectsReadme repoUrl={projectList[projIdx].url} />
</>
),
},
{
id: "systems",
title: "Systems",
content: (
<>
<Systems />
</>
),
},
{ {
id: "about", id: "about",
title: "About", title: "About",
@@ -21,15 +90,6 @@ export default function Home(): ReactElement {
</> </>
), ),
}, },
{
id: "current-activity",
title: "Current-activity",
content: (
<>
<CurrentActivity />
</>
),
},
]; ];
return ( return (
@@ -38,6 +98,14 @@ export default function Home(): ReactElement {
<h1 className="text-4xl font-bold text-ctp-mauve"> <h1 className="text-4xl font-bold text-ctp-mauve">
AramJonghu AramJonghu
</h1> </h1>
<p className="mt-4">
Software engineer building systems, services, and
experiments across a self-hosted environment.
</p>
<p className="mt-3">
This site acts as a hub for projects, infrastructure, and
personal tooling.
</p>
<div className="mt-10 border rounded p-4"> <div className="mt-10 border rounded p-4">
<div className="flex flex-wrap justify-center gap-12 mb-6"> <div className="flex flex-wrap justify-center gap-12 mb-6">
{sections.map((section) => ( {sections.map((section) => (
-247
View File
@@ -1,247 +0,0 @@
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>
);
}
+1 -1
View File
@@ -50,7 +50,7 @@ export default function Stream(): ReactElement {
<div className="stream-frame w-full mx-auto flex-1 max-h-[85vh]"> <div className="stream-frame w-full mx-auto flex-1 max-h-[85vh]">
<iframe <iframe
src={`https://stream.aramjonghu.dev/${activeStream}`} src={`https://stream.aramjonghu.nl/${activeStream}`}
title={activeStream} title={activeStream}
allow="autoplay; fullscreen; picture-in-picture" allow="autoplay; fullscreen; picture-in-picture"
className="w-full h-full border-0 rounded-lg" className="w-full h-full border-0 rounded-lg"
-50
View File
@@ -1,50 +0,0 @@
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;
}