Skip to content

Commit beb93f2

Browse files
committed
Version 2.0.2
1 parent 6a84acd commit beb93f2

12 files changed

Lines changed: 3234 additions & 741 deletions

File tree

.github/instructions/app-router.instructions.md

Lines changed: 1562 additions & 0 deletions
Large diffs are not rendered by default.

.github/instructions/atomic-components.instructions.md

Lines changed: 741 additions & 0 deletions
Large diffs are not rendered by default.

.github/instructions/frontend-workflow.instructions.md

Lines changed: 453 additions & 520 deletions
Large diffs are not rendered by default.
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1-
import { Github, Linkedin, Mail, Twitter } from "lucide-react"
2-
import { SocialIcon } from "@/components/atoms/social-icon"
1+
import { SocialIcon } from '@/components/atoms/social-icon';
2+
import { Github, Linkedin, Mail } from 'lucide-react';
33

44
export function SocialLinks() {
55
return (
66
<div className="flex items-center gap-4">
7-
<SocialIcon href="https://github.com" icon={Github} label="GitHub" />
8-
<SocialIcon href="https://linkedin.com" icon={Linkedin} label="LinkedIn" />
9-
<SocialIcon href="https://twitter.com" icon={Twitter} label="Twitter" />
10-
<SocialIcon href="mailto:contact@example.com" icon={Mail} label="Email" />
7+
<SocialIcon
8+
href="https://github.com/ollehmichael"
9+
icon={Github}
10+
label="GitHub"
11+
/>
12+
<SocialIcon
13+
href="https://www.linkedin.com/in/byong-cheol-han-60127b1b7/"
14+
icon={Linkedin}
15+
label="LinkedIn"
16+
/>
17+
<SocialIcon
18+
href="mailto:byongcheolhan@gmail.com"
19+
icon={Mail}
20+
label="Email"
21+
/>
1122
</div>
12-
)
23+
);
1324
}

components/organisms/career-timeline.tsx

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
'use client';
22

3+
import { CareerItem } from '@/lib/careerTimeline';
4+
import { Projects } from '@/lib/projects';
35
import { Building2, Calendar, ChevronDown, ExternalLink } from 'lucide-react';
6+
import Image from 'next/image';
47
import Link from 'next/link';
58
import { useState } from 'react';
69

7-
interface CareerItem {
8-
startDate: string;
9-
endDate: string | null;
10-
position: string;
11-
company: string;
12-
description: string;
13-
projectIds?: string[];
14-
}
15-
1610
interface CareerProgressionProps {
1711
items: CareerItem[];
1812
}
@@ -36,31 +30,62 @@ export default function CareerProgression({ items }: CareerProgressionProps) {
3630
<div className="mx-auto">
3731
<div className="relative ml-3">
3832
{/* Timeline line */}
39-
<div className="absolute left-0 top-7 bottom-0 border-l-2" />
33+
<div className="absolute top-7 bottom-0 left-0 border-l-2" />
4034

4135
{items.map(
4236
(
43-
{ company, position, description, startDate, endDate, projectIds },
37+
{
38+
company,
39+
companyLink,
40+
highlightLinks,
41+
logo,
42+
position,
43+
description,
44+
startDate,
45+
endDate,
46+
projectIds,
47+
},
4448
index
4549
) => {
4650
const isExpanded = expandedItems.has(index);
4751
const truncatedDescription = description.slice(0, 90) + '...';
4852

4953
return (
50-
<div key={index} className="relative pl-8 mb-12 last:mb-0">
54+
<div key={index} className="relative mb-12 pl-8 last:mb-0">
5155
{/* Timeline dot */}
52-
<div className="absolute h-3 w-3 -translate-x-1/2 left-px top-7 rounded-full border-2 border-primary bg-background" />
56+
<div className="border-primary bg-background absolute top-7 left-px h-3 w-3 -translate-x-1/2 rounded-full border-2" />
5357

5458
{/* Content - Clickable Card */}
5559
<div
5660
onClick={() => toggleItem(index)}
57-
className="space-y-3 cursor-pointer rounded-lg p-4 -ml-4 border border-transparent hover:border-primary/20 hover:bg-accent/30 transition-all duration-200"
61+
className="hover:border-primary/20 hover:bg-accent/30 -ml-4 cursor-pointer space-y-3 rounded-lg border border-transparent p-4 transition-all duration-200"
5862
>
5963
<div className="flex items-center gap-2.5">
60-
<div className="shrink-0 h-9 w-9 bg-accent rounded-full flex items-center justify-center">
61-
<Building2 className="h-5 w-5 text-muted" />
64+
<div
65+
className={`${logo ? 'bg-accent-foreground' : 'bg-accent'} flex h-9 w-9 shrink-0 items-center justify-center rounded-full`}
66+
>
67+
{logo ? (
68+
<Image
69+
src={logo}
70+
alt={`${company} logo`}
71+
className="text-muted h-8 w-8 object-contain"
72+
/>
73+
) : (
74+
<Building2 className="text-muted h-5 w-5" />
75+
)}
6276
</div>
6377
<span className="text-base font-medium">{company}</span>
78+
{companyLink && (
79+
<Link
80+
href={companyLink}
81+
onClick={(e) => e.stopPropagation()}
82+
target="_blank"
83+
rel="noopener noreferrer"
84+
className="hover:text-primary h-5 w-5 text-base"
85+
>
86+
<ExternalLink size={16} />
87+
</Link>
88+
)}
6489
<ChevronDown
6590
className={`ml-auto h-5 w-5 transition-transform duration-200 ${
6691
isExpanded ? 'rotate-180' : ''
@@ -69,13 +94,20 @@ export default function CareerProgression({ items }: CareerProgressionProps) {
6994
</div>
7095
<div>
7196
<h3 className="text-lg font-semibold">{position}</h3>
72-
<div className="flex items-center gap-2 mt-2 text-sm">
97+
<div className="mt-2 flex items-center gap-2 text-sm">
7398
<Calendar className="h-4 w-4" />
74-
<span>{`${startDate} - ${endDate || 'PRESENT'}`}</span>
99+
<span>
100+
<span>{`${startDate} - `}</span>
101+
{endDate ? (
102+
<span>{endDate}</span>
103+
) : (
104+
<span className="text-primary">{'PRESENT'}</span>
105+
)}
106+
</span>
75107
</div>
76108
</div>
77109
<div className="overflow-hidden">
78-
<p className="text-sm sm:text-base text-muted-foreground text-pretty transition-all duration-700 ease-in-out">
110+
<p className="text-muted-foreground text-sm text-pretty transition-all duration-700 ease-in-out sm:text-base">
79111
{isExpanded ? description : truncatedDescription}
80112
</p>
81113
</div>
@@ -92,9 +124,16 @@ export default function CareerProgression({ items }: CareerProgressionProps) {
92124
key={projectId}
93125
href={`/projects/${projectId}`}
94126
onClick={(e) => e.stopPropagation()}
95-
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-primary/10 text-primary border border-primary/20 rounded-md hover:bg-primary/20 transition-colors"
127+
className="bg-primary/10 text-primary border-primary/20 hover:bg-primary/20 inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors"
96128
>
97-
<span> {projectId.replace(/-/g, ' ')}</span>
129+
<span>
130+
{' '}
131+
{
132+
Projects.find(
133+
(project) => project.id === projectId
134+
)?.title
135+
}
136+
</span>
98137
<ExternalLink size={12} />
99138
</Link>
100139
))}

components/templates/career.tsx

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,87 @@
1+
'use client';
2+
13
import CareerProgression from '@/components/organisms/career-timeline';
24
import { CareerTimeline } from '@/lib/careerTimeline';
5+
import { useEffect, useState } from 'react';
36

47
export default function CareerView() {
8+
const [experience, setExperience] = useState({
9+
years: 0,
10+
months: 0,
11+
days: 0,
12+
});
13+
14+
useEffect(() => {
15+
const calculateExperience = () => {
16+
const startDate = new Date('2020-06-01');
17+
const now = new Date();
18+
19+
let years = now.getFullYear() - startDate.getFullYear();
20+
let months = now.getMonth() - startDate.getMonth();
21+
let days = now.getDate() - startDate.getDate();
22+
23+
// Adjust for negative days
24+
if (days < 0) {
25+
months--;
26+
const prevMonth = new Date(now.getFullYear(), now.getMonth(), 0);
27+
days += prevMonth.getDate();
28+
}
29+
30+
// Adjust for negative months
31+
if (months < 0) {
32+
years--;
33+
months += 12;
34+
}
35+
36+
setExperience({ years, months, days });
37+
};
38+
39+
calculateExperience();
40+
41+
// Update daily at midnight
42+
const now = new Date();
43+
const tomorrow = new Date(
44+
now.getFullYear(),
45+
now.getMonth(),
46+
now.getDate() + 1
47+
);
48+
const timeUntilMidnight = tomorrow.getTime() - now.getTime();
49+
50+
const midnightTimeout = setTimeout(() => {
51+
calculateExperience();
52+
// Then update every 24 hours
53+
const dailyInterval = setInterval(
54+
calculateExperience,
55+
24 * 60 * 60 * 1000
56+
);
57+
return () => clearInterval(dailyInterval);
58+
}, timeUntilMidnight);
59+
60+
return () => clearTimeout(midnightTimeout);
61+
}, []);
62+
563
return (
664
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6 md:py-24 lg:px-8">
765
<div className="space-y-12 md:space-y-16">
866
{/* Header */}
967
<div className="animate-fade-in space-y-4">
1068
<h1 className="text-4xl font-bold sm:text-5xl md:text-6xl">
11-
Career <span className="text-primary">Journey</span>
69+
{"Mike's "}
70+
<span className="text-primary">{'Journey'}</span>
71+
</h1>
72+
<h1 className="text-muted-foreground text-lg md:text-xl">
73+
<span className="text-foreground font-bold">
74+
{`${experience.years}${experience.years === 1 ? 'year' : 'years'}
75+
${experience.months}${experience.months === 1 ? 'month' : 'months'}
76+
${experience.days}${experience.days === 1 ? 'day' : 'days'}`}
77+
</span>
78+
{' and '}
79+
<span className="text-foreground font-bold">{'counting...'}</span>
1280
</h1>
1381
<p className="text-muted-foreground max-w-3xl text-lg md:text-xl">
14-
A timeline of my professional experience, from military service to
15-
software engineering leadership.
82+
{
83+
'A timeline of my professional experience, from military service to where I am now.'
84+
}
1685
</p>
1786
</div>
1887

components/templates/projectDetails.tsx

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,95 @@ export default async function ProjectDetailsView({
1010
}: {
1111
project: Project;
1212
}) {
13+
// Helper function to detect paragraph type and render appropriately
14+
function renderFormattedParagraph(paragraph: string, index: number) {
15+
// Empty string - render as line break
16+
if (paragraph.trim() === '') {
17+
return <br key={index} />;
18+
}
19+
20+
// Heading with # markdown syntax (## Heading)
21+
if (paragraph.startsWith('#')) {
22+
const level = paragraph.match(/^#+/)?.[0].length || 1;
23+
const text = paragraph.replace(/^#+\s*/, '');
24+
25+
if (level === 1) {
26+
return (
27+
<h2 key={index} className="mt-8 text-2xl font-bold first:mt-0">
28+
{text}
29+
</h2>
30+
);
31+
} else {
32+
return (
33+
<h3 key={index} className="mt-6 text-xl font-semibold first:mt-0">
34+
{text}
35+
</h3>
36+
);
37+
}
38+
}
39+
40+
// ALL CAPS heading with colon (OVERVIEW:)
41+
if (/^[A-Z\s]+:/.test(paragraph)) {
42+
const text = paragraph.replace(':', '');
43+
return (
44+
<h3
45+
key={index}
46+
className="mt-6 text-lg font-bold tracking-wide uppercase first:mt-0"
47+
>
48+
{text}
49+
</h3>
50+
);
51+
}
52+
53+
// List item (starts with •, -, or *)
54+
if (/^[\-*]\s/.test(paragraph)) {
55+
const text = paragraph.replace(/^[\-*]\s*/, '');
56+
return (
57+
<li
58+
key={index}
59+
className="text-muted-foreground ml-6 text-base leading-relaxed"
60+
>
61+
{renderInlineFormatting(text)}
62+
</li>
63+
);
64+
}
65+
66+
// Regular paragraph with inline formatting
67+
return (
68+
<p
69+
key={index}
70+
className="text-muted-foreground text-base leading-relaxed"
71+
>
72+
{renderInlineFormatting(paragraph)}
73+
</p>
74+
);
75+
}
76+
77+
// Helper function to render inline formatting (bold, italic, etc.)
78+
function renderInlineFormatting(text: string) {
79+
// Split by ** for bold text
80+
const parts = text.split(/(\*\*.*?\*\*)/g);
81+
82+
return parts.map((part, i) => {
83+
// Bold text wrapped in **
84+
if (part.startsWith('**') && part.endsWith('**')) {
85+
const content = part.slice(2, -2);
86+
return (
87+
<strong key={i} className="text-foreground font-semibold">
88+
{content}
89+
</strong>
90+
);
91+
}
92+
93+
// Check for HTML strong tags
94+
if (part.includes('<strong>') || part.includes('<em>')) {
95+
return <span key={i} dangerouslySetInnerHTML={{ __html: part }} />;
96+
}
97+
98+
return part;
99+
});
100+
}
101+
13102
return (
14103
<div className="mx-auto max-w-5xl px-4 pb-12 sm:px-6 md:pb-20 lg:px-8">
15104
{/* Back Button and Title */}
@@ -77,14 +166,9 @@ export default async function ProjectDetailsView({
77166

78167
{/* Full Description */}
79168
<div className="space-y-4">
80-
{project.fullDescription.map((paragraph, index) => (
81-
<p
82-
key={index}
83-
className="text-muted-foreground text-base leading-relaxed"
84-
>
85-
{paragraph}
86-
</p>
87-
))}
169+
{project.fullDescription.map((paragraph, index) =>
170+
renderFormattedParagraph(paragraph, index)
171+
)}
88172
</div>
89173

90174
{/* Tech Stack */}

0 commit comments

Comments
 (0)