mirror of
https://git.aramjonghu.dev/AramJonghu/aramjonghu-site.git
synced 2026-09-13 17:53:31 +02:00
feat(Portfolio): implementation of Portfolio page
This commit is contained in:
@@ -80,3 +80,7 @@
|
|||||||
color: var(--shiki-dark);
|
color: var(--shiki-dark);
|
||||||
background-color: var(--shiki-dark-bg);
|
background-color: var(--shiki-dark-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.macchiato .icon-invert {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 Contact from "./pages/Contact";
|
||||||
|
import Portfolio from "./pages/Portfolio";
|
||||||
|
|
||||||
export default function App(): ReactElement {
|
export default function App(): ReactElement {
|
||||||
const path = window.location.pathname.toLowerCase();
|
const path = window.location.pathname.toLowerCase();
|
||||||
@@ -18,5 +19,9 @@ export default function App(): ReactElement {
|
|||||||
content = <Contact />;
|
content = <Contact />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === "/portfolio") {
|
||||||
|
content = <Portfolio />;
|
||||||
|
}
|
||||||
|
|
||||||
return <Layout>{content}</Layout>;
|
return <Layout>{content}</Layout>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<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="w-full max-w-360 min-w-120 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-[2fr_1fr] gap-8 items-start">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<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">
|
||||||
|
<div className="bg-ctp-mantle border border-ctp-lavender-800 rounded-xl p-6">
|
||||||
|
<div className="size-20 rounded-full bg-ctp-surface0 border border-ctp-text flex items-center justify-center text-2xl font-bold text-ctp-mauve mb-4">
|
||||||
|
{profile.name[0]}
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user