mirror of
https://git.aramjonghu.dev/AramJonghu/aramjonghu-site.git
synced 2026-09-06 07:33:29 +02:00
feat(Portfolio): implementation of Portfolio page
This commit is contained in:
@@ -80,3 +80,7 @@
|
||||
color: var(--shiki-dark);
|
||||
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 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 = <Contact />;
|
||||
}
|
||||
|
||||
if (path === "/portfolio") {
|
||||
content = <Portfolio />;
|
||||
}
|
||||
|
||||
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