Skip to content

Commit 2ba661e

Browse files
yahyuneeclaude
andcommitted
Restructure nav to About / Blog / Contact and add blog page
- Collapse navigation to three items: About (index.html), Blog (blog.html), Contact - Add new blog.html that renders posts from posts/posts.json - Add blog.js for dynamic rendering with date sorting and image galleries - Add posts/README.md documenting the post schema and workflow - Seed first post: "Blog section opened!" - Add active nav state styling and full blog page CSS Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c8f1d96 commit 2ba661e

6 files changed

Lines changed: 315 additions & 6 deletions

File tree

blog.html

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Blog - Ahhyun Lucy Lee</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9+
</head>
10+
<body>
11+
<!-- Navigation -->
12+
<nav class="navbar">
13+
<div class="nav-container">
14+
<a href="index.html" class="nav-logo">Ahhyun Lucy Lee</a>
15+
<ul class="nav-menu">
16+
<li><a href="index.html" class="nav-link">About</a></li>
17+
<li><a href="blog.html" class="nav-link active">Blog</a></li>
18+
<li><a href="index.html#contact" class="nav-link">Contact</a></li>
19+
</ul>
20+
</div>
21+
</nav>
22+
23+
<!-- Blog Hero -->
24+
<section class="blog-hero">
25+
<div class="container">
26+
<h1 class="blog-page-title">Blog</h1>
27+
<p class="blog-page-subtitle">Moments from research, conferences, travel, and life.</p>
28+
</div>
29+
</section>
30+
31+
<!-- Blog Posts -->
32+
<section class="section">
33+
<div class="container">
34+
<div id="posts-container" class="posts-list"></div>
35+
<div id="empty-state" class="blog-empty" style="display:none;">
36+
<i class="fas fa-pen-nib"></i>
37+
<p>No posts yet — check back soon!</p>
38+
</div>
39+
</div>
40+
</section>
41+
42+
<!-- Footer -->
43+
<footer class="footer">
44+
<p>&copy; 2026 Ahhyun Lucy Lee. All rights reserved.</p>
45+
<p class="footer-note">Last updated: May 2026</p>
46+
</footer>
47+
48+
<script src="blog.js"></script>
49+
</body>
50+
</html>

blog.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Load and render blog posts from posts/posts.json
2+
async function loadPosts() {
3+
const container = document.getElementById('posts-container');
4+
const emptyState = document.getElementById('empty-state');
5+
6+
try {
7+
const response = await fetch('posts/posts.json', { cache: 'no-cache' });
8+
if (!response.ok) throw new Error('Could not load posts');
9+
const posts = await response.json();
10+
11+
if (!posts || posts.length === 0) {
12+
emptyState.style.display = 'flex';
13+
return;
14+
}
15+
16+
// Sort newest first
17+
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
18+
container.innerHTML = posts.map(renderPost).join('');
19+
} catch (err) {
20+
console.error(err);
21+
emptyState.style.display = 'flex';
22+
}
23+
}
24+
25+
function renderPost(post) {
26+
const dateStr = formatDate(post.date);
27+
const meta = post.location ? `${dateStr} · ${post.location}` : dateStr;
28+
29+
const imagesHtml = (post.images && post.images.length > 0)
30+
? `<div class="blog-post-gallery ${galleryClass(post.images.length)}">
31+
${post.images.map(img => `
32+
<div class="blog-post-image">
33+
<img src="posts/${img}" alt="${escapeHtml(post.title)}" loading="lazy">
34+
</div>
35+
`).join('')}
36+
</div>`
37+
: '';
38+
39+
return `
40+
<article class="blog-post" id="post-${escapeHtml(post.id)}">
41+
<header class="blog-post-header">
42+
<h2 class="blog-post-title">${escapeHtml(post.title)}</h2>
43+
<p class="blog-post-meta">${escapeHtml(meta)}</p>
44+
</header>
45+
${imagesHtml}
46+
<p class="blog-post-description">${escapeHtml(post.description)}</p>
47+
</article>
48+
`;
49+
}
50+
51+
function galleryClass(count) {
52+
if (count === 1) return 'gallery-1';
53+
if (count === 2) return 'gallery-2';
54+
return 'gallery-multi';
55+
}
56+
57+
function formatDate(d) {
58+
const date = new Date(d);
59+
if (isNaN(date)) return d;
60+
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
61+
}
62+
63+
function escapeHtml(str) {
64+
if (str == null) return '';
65+
return String(str)
66+
.replace(/&/g, '&amp;')
67+
.replace(/</g, '&lt;')
68+
.replace(/>/g, '&gt;')
69+
.replace(/"/g, '&quot;')
70+
.replace(/'/g, '&#39;');
71+
}
72+
73+
// Navbar scroll shadow (shared behavior with index.html)
74+
const navbar = document.querySelector('.navbar');
75+
window.addEventListener('scroll', () => {
76+
if (window.pageYOffset > 50) {
77+
navbar.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.3)';
78+
} else {
79+
navbar.style.boxShadow = 'none';
80+
}
81+
});
82+
83+
loadPosts();

index.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@
1111
<!-- Navigation -->
1212
<nav class="navbar">
1313
<div class="nav-container">
14-
<a href="#home" class="nav-logo">Ahhyun Lucy Lee</a>
14+
<a href="index.html" class="nav-logo">Ahhyun Lucy Lee</a>
1515
<ul class="nav-menu">
16-
<li><a href="#about" class="nav-link">About</a></li>
17-
<li><a href="#research" class="nav-link">Research</a></li>
18-
<li><a href="#projects" class="nav-link">Projects</a></li>
19-
<li><a href="#skills" class="nav-link">Skills</a></li>
20-
<li><a href="#dance" class="nav-link">Activities</a></li>
16+
<li><a href="index.html" class="nav-link active">About</a></li>
17+
<li><a href="blog.html" class="nav-link">Blog</a></li>
2118
<li><a href="#contact" class="nav-link">Contact</a></li>
2219
</ul>
2320
</div>

posts/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Blog Posts
2+
3+
Blog posts are stored in `posts.json`. Each post is an object with this schema:
4+
5+
```json
6+
{
7+
"id": "unique-slug", // unique identifier (kebab-case)
8+
"title": "Post title",
9+
"date": "YYYY-MM-DD", // used for sorting (newest first)
10+
"location": "Optional place", // shown after the date (optional, omit if not applicable)
11+
"description": "Short paragraph...", // 1-3 sentences
12+
"images": ["images/file1.jpg", "..."] // paths relative to /posts/, can be empty []
13+
}
14+
```
15+
16+
## How to add a new post
17+
18+
1. Drop image files into `posts/images/`.
19+
2. Add a new entry to the top of the array in `posts.json` (newest posts first; the page also sorts by `date` automatically).
20+
3. Commit and push — GitHub Pages will redeploy.
21+
22+
Or just tell Claude: "Add a blog post about X at [location] on [date] with these pictures: ..." and Claude will handle everything.

posts/posts.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"id": "blog-opened",
4+
"title": "Blog section opened!",
5+
"date": "2026-05-25",
6+
"location": "Princeton, NJ",
7+
"description": "Just opened up this blog to share little moments from research, conferences, travel, and life. Stay tuned for posts about workshops, hackathons, lab visits, and everything in between!",
8+
"images": []
9+
}
10+
]

styles.css

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ body {
9999
width: 100%;
100100
}
101101

102+
.nav-link.active {
103+
color: var(--white);
104+
}
105+
106+
.nav-link.active::after {
107+
width: 100%;
108+
}
109+
102110
/* Hero Section */
103111
.hero {
104112
min-height: 100vh;
@@ -681,6 +689,145 @@ body {
681689
font-size: 0.95rem;
682690
}
683691

692+
/* Blog Page */
693+
.blog-hero {
694+
background: linear-gradient(135deg, var(--burgundy-dark) 0%, var(--burgundy-medium) 100%);
695+
color: var(--white);
696+
padding: 10rem 2rem 4rem;
697+
text-align: center;
698+
}
699+
700+
.blog-page-title {
701+
font-size: 3.5rem;
702+
margin-bottom: 0.75rem;
703+
color: var(--white);
704+
}
705+
706+
.blog-page-subtitle {
707+
font-size: 1.2rem;
708+
color: var(--off-white);
709+
opacity: 0.9;
710+
}
711+
712+
.posts-list {
713+
display: flex;
714+
flex-direction: column;
715+
gap: 3rem;
716+
max-width: 800px;
717+
margin: 0 auto;
718+
}
719+
720+
.blog-post {
721+
background-color: rgba(107, 31, 58, 0.03);
722+
border: 1px solid rgba(107, 31, 58, 0.15);
723+
border-radius: 10px;
724+
padding: 2rem;
725+
transition: transform 0.3s ease, border-color 0.3s ease;
726+
}
727+
728+
.blog-post:hover {
729+
transform: translateY(-3px);
730+
border-color: var(--burgundy-medium);
731+
}
732+
733+
.blog-post-header {
734+
margin-bottom: 1.5rem;
735+
}
736+
737+
.blog-post-title {
738+
font-size: 1.8rem;
739+
color: var(--burgundy-dark);
740+
margin-bottom: 0.4rem;
741+
line-height: 1.3;
742+
}
743+
744+
.blog-post-meta {
745+
color: var(--burgundy-medium);
746+
font-size: 0.95rem;
747+
font-style: italic;
748+
}
749+
750+
.blog-post-description {
751+
color: var(--gray-medium);
752+
line-height: 1.8;
753+
font-size: 1.05rem;
754+
margin-top: 1.5rem;
755+
}
756+
757+
.blog-post-gallery {
758+
display: grid;
759+
gap: 0.75rem;
760+
margin: 1.5rem 0;
761+
}
762+
763+
.blog-post-gallery.gallery-1 {
764+
grid-template-columns: 1fr;
765+
}
766+
767+
.blog-post-gallery.gallery-2 {
768+
grid-template-columns: 1fr 1fr;
769+
}
770+
771+
.blog-post-gallery.gallery-multi {
772+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
773+
}
774+
775+
.blog-post-image {
776+
overflow: hidden;
777+
border-radius: 8px;
778+
background-color: var(--off-white);
779+
}
780+
781+
.blog-post-image img {
782+
width: 100%;
783+
height: 100%;
784+
object-fit: cover;
785+
display: block;
786+
transition: transform 0.4s ease;
787+
}
788+
789+
.blog-post-image:hover img {
790+
transform: scale(1.03);
791+
}
792+
793+
.blog-empty {
794+
display: flex;
795+
flex-direction: column;
796+
align-items: center;
797+
justify-content: center;
798+
gap: 1rem;
799+
padding: 4rem 2rem;
800+
color: var(--gray-medium);
801+
text-align: center;
802+
}
803+
804+
.blog-empty i {
805+
font-size: 3rem;
806+
color: var(--burgundy-accent);
807+
}
808+
809+
.blog-empty p {
810+
font-size: 1.1rem;
811+
}
812+
813+
@media (max-width: 768px) {
814+
.blog-page-title {
815+
font-size: 2.5rem;
816+
}
817+
818+
.blog-post {
819+
padding: 1.5rem;
820+
}
821+
822+
.blog-post-title {
823+
font-size: 1.5rem;
824+
}
825+
826+
.blog-post-gallery.gallery-2 {
827+
grid-template-columns: 1fr;
828+
}
829+
}
830+
684831
/* Footer */
685832
.footer {
686833
background-color: var(--burgundy-dark);

0 commit comments

Comments
 (0)