From fec4ed568c5bc52c922386200dc3724506e69171 Mon Sep 17 00:00:00 2001 From: AramJonghu Date: Thu, 6 Aug 2026 18:33:32 +0200 Subject: [PATCH] feat(Portfolio): implementation of Portfolio page --- frontend/src/App.css | 4 + frontend/src/App.tsx | 5 + frontend/src/pages/Portfolio.tsx | 244 +++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 frontend/src/pages/Portfolio.tsx diff --git a/frontend/src/App.css b/frontend/src/App.css index 95b3ee8..3a04a8d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -80,3 +80,7 @@ color: var(--shiki-dark); background-color: var(--shiki-dark-bg); } + +.macchiato .icon-invert { + filter: invert(1); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 57c7108..a959e47 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ 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"; export default function App(): ReactElement { const path = window.location.pathname.toLowerCase(); @@ -18,5 +19,9 @@ export default function App(): ReactElement { content = ; } + if (path === "/portfolio") { + content = ; + } + return {content}; } diff --git a/frontend/src/pages/Portfolio.tsx b/frontend/src/pages/Portfolio.tsx new file mode 100644 index 0000000..270b15c --- /dev/null +++ b/frontend/src/pages/Portfolio.tsx @@ -0,0 +1,244 @@ +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"; + +const LOGOS: Record = { 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 ( +
+
+

+ Portfolio +

+ +
+
+ + {projects.map((proj) => ( + +

+ {proj.name} +

+

+ {proj.description} +

+
+ {proj.tags.map((tag) => ( + + ))} +
+
+ ))} +
+ + {skills.map((group) => ( +
+

+ {group.category} +

+
+ {group.items.map((skill) => ( + + ))} +
+
+ ))} +
+ + {education.map((edu) => ( + + ))} + + + {experience.map((exp) => ( + + ))} + +
+
+
+
+ {profile.name[0]} +
+

+ {profile.name} +

+

+ {profile.headline} +

+

+ {profile.location} +

+

+ {profile.region} +

+
+ {profile.links.map((link) => ( + + {link.label} + + ))} +
+
+ + +

+ {profile.summary} +

+
+ + +
    + {languages.map((lang) => ( +
  • + {lang.name} + + {lang.proficiency} + +
  • + ))} +
+
+
+
+
+
+ ); +}