Skip to content

Commit 07bab7b

Browse files
committed
a major update to the project
1 parent 74f1ac5 commit 07bab7b

34 files changed

Lines changed: 2249 additions & 422 deletions

public/assets/resume.pdf

120 KB
Binary file not shown.

public/images/avatar.jpg

350 KB
Loading
1.59 MB
Loading

public/my_avatar.png

1.38 MB
Loading

src/components/ColorBar.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import React, { useEffect } from 'react';
2-
import '../styles/colorbar.css';
32

43
const gradients = [
54
{
6-
label: '默认',
5+
label: 'Default',
76
value: 'linear-gradient(45deg, rgb(var(--accent)), #da62c4 30%, white 60%)',
87
accent: '124, 58, 237',
98
darkValue: 'linear-gradient(45deg, rgb(var(--accent)), #da62c4 30%, #2a2a2a 60%)'
109
},
1110
{
12-
label: '蓝色',
11+
label: 'Blue',
1312
value: 'linear-gradient(45deg, #00b4d8, #48cae4 30%, white 60%)',
1413
accent: '0, 180, 216',
1514
darkValue: 'linear-gradient(45deg, #00b4d8, #48cae4 30%, #2a2a2a 60%)'
1615
},
1716
{
18-
label: '绿色',
17+
label: 'Green',
1918
value: 'linear-gradient(45deg, #16a34a, #4ade80 30%, white 60%)',
2019
accent: '22, 163, 74',
2120
darkValue: 'linear-gradient(45deg, #16a34a, #4ade80 30%, #2a2a2a 60%)'

src/components/Lightbox.jsx

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import React, { useEffect, useState, useCallback, useRef } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
const Lightbox = ({ photos, currentIndex, onClose }) => {
5+
const [activeIndex, setActiveIndex] = useState(currentIndex);
6+
const [transitionClass, setTransitionClass] = useState('');
7+
const [preloadedImages, setPreloadedImages] = useState({});
8+
const lightboxRef = useRef(null);
9+
10+
// Handle remote image URLs
11+
const getOptimizedImageUrl = (src) => {
12+
if (src.startsWith('http') && src.includes('unsplash.com')) {
13+
return `${src}?w=1200&q=90&auto=format`; // High quality version for fullscreen viewing
14+
}
15+
return src;
16+
};
17+
18+
// Preload adjacent images
19+
const preloadAdjacentImages = useCallback(() => {
20+
const indicesToLoad = [
21+
activeIndex - 1,
22+
activeIndex + 1
23+
].filter(idx => idx >= 0 && idx < photos.length);
24+
25+
indicesToLoad.forEach(idx => {
26+
if (!preloadedImages[idx]) {
27+
const img = new Image();
28+
img.src = getOptimizedImageUrl(photos[idx].src);
29+
setPreloadedImages(prev => ({
30+
...prev,
31+
[idx]: true
32+
}));
33+
}
34+
});
35+
}, [activeIndex, photos, preloadedImages]);
36+
37+
// Handle keyboard navigation
38+
useEffect(() => {
39+
const handleKeyDown = (e) => {
40+
switch (e.key) {
41+
case 'ArrowLeft':
42+
navigateImage('prev');
43+
break;
44+
case 'ArrowRight':
45+
navigateImage('next');
46+
break;
47+
case 'Escape':
48+
onClose();
49+
break;
50+
default:
51+
break;
52+
}
53+
};
54+
55+
window.addEventListener('keydown', handleKeyDown);
56+
return () => window.removeEventListener('keydown', handleKeyDown);
57+
}, [activeIndex, photos.length, onClose]);
58+
59+
// Preload adjacent images
60+
useEffect(() => {
61+
preloadAdjacentImages();
62+
}, [activeIndex, preloadAdjacentImages]);
63+
64+
// Handle image navigation
65+
const navigateImage = (direction) => {
66+
if (direction === 'prev' && activeIndex > 0) {
67+
setTransitionClass('transition-prev');
68+
setTimeout(() => setActiveIndex(activeIndex - 1), 50);
69+
} else if (direction === 'next' && activeIndex < photos.length - 1) {
70+
setTransitionClass('transition-next');
71+
setTimeout(() => setActiveIndex(activeIndex + 1), 50);
72+
}
73+
};
74+
75+
// Handle touch events
76+
const [touchStart, setTouchStart] = useState(null);
77+
const handleTouchStart = (e) => {
78+
setTouchStart(e.touches[0].clientX);
79+
};
80+
81+
const handleTouchEnd = (e) => {
82+
if (!touchStart) return;
83+
84+
const touchEnd = e.changedTouches[0].clientX;
85+
const diff = touchStart - touchEnd;
86+
const threshold = 50;
87+
88+
if (diff > threshold) {
89+
navigateImage('next');
90+
} else if (diff < -threshold) {
91+
navigateImage('prev');
92+
}
93+
94+
setTouchStart(null);
95+
};
96+
97+
// Handle click to close
98+
const handleOverlayClick = (e) => {
99+
if (e.target === lightboxRef.current) {
100+
onClose();
101+
}
102+
};
103+
104+
return (
105+
<div
106+
className={`lightbox-overlay active`}
107+
ref={lightboxRef}
108+
onClick={handleOverlayClick}
109+
onTouchStart={handleTouchStart}
110+
onTouchEnd={handleTouchEnd}
111+
>
112+
<div className="lightbox-container">
113+
{photos.map((photo, index) => (
114+
<img
115+
key={`lightbox-${index}`}
116+
src={getOptimizedImageUrl(photo.src)}
117+
alt={photo.alt || 'Image'}
118+
className={`lightbox-img ${index === activeIndex ? 'active ' + transitionClass : ''}`}
119+
style={{ display: index === activeIndex ? 'block' : 'none' }}
120+
referrerPolicy={photo.src.startsWith('http') ? "no-referrer" : ""}
121+
loading={Math.abs(index - activeIndex) <= 1 ? "eager" : "lazy"}
122+
/>
123+
))}
124+
125+
{photos[activeIndex]?.caption && (
126+
<div className={`lightbox-caption ${activeIndex === currentIndex ? '' : 'active'}`}>
127+
{photos[activeIndex].caption}
128+
</div>
129+
)}
130+
</div>
131+
132+
<div className="lightbox-close" onClick={onClose}></div>
133+
134+
<div className="lightbox-navigation">
135+
<button
136+
className={`lightbox-nav-btn lightbox-prev ${activeIndex === 0 ? 'disabled' : ''}`}
137+
onClick={() => navigateImage('prev')}
138+
disabled={activeIndex === 0}
139+
aria-label="Previous image"
140+
></button>
141+
<button
142+
className={`lightbox-nav-btn lightbox-next ${activeIndex === photos.length - 1 ? 'disabled' : ''}`}
143+
onClick={() => navigateImage('next')}
144+
disabled={activeIndex === photos.length - 1}
145+
aria-label="Next image"
146+
></button>
147+
</div>
148+
</div>
149+
);
150+
};
151+
152+
Lightbox.propTypes = {
153+
photos: PropTypes.array.isRequired,
154+
currentIndex: PropTypes.number.isRequired,
155+
onClose: PropTypes.func.isRequired
156+
};
157+
158+
export default Lightbox;

src/components/Nav.astro

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
---
22
import HeaderLink from "./HeaderLink.astro";
33
import ThemeToggle from "./ThemeToggle.astro";
4-
import "../styles/navigation.css";
4+
import "../styles/components/navigation.css";
55
---
66
<header class="site-header">
7-
<div class="hidden-nav-container">
8-
<nav class="hidden-nav">
7+
<nav class="main-nav">
98
<div class="nav-wrapper">
109
<div class="flex gap-4">
1110
<HeaderLink href="/">
@@ -17,14 +16,19 @@ import "../styles/navigation.css";
1716
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1817
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2.5 2.5 0 00-2.5-2.5H15" />
1918
</svg>
19+
</HeaderLink>
2020
<HeaderLink href="/projects">
2121
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2222
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
2323
</svg>
2424
</HeaderLink>
25+
<HeaderLink href="/about">
26+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
27+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
28+
</svg>
29+
</HeaderLink>
2530
</div>
2631
<ThemeToggle />
2732
</div>
2833
</nav>
29-
</div>
3034
</header>

src/components/Photo.jsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { useState } from 'react';
22
import PropTypes from 'prop-types';
33

4-
const Photo = ({ src, alt, caption, onLoad, onSize }) => {
4+
const Photo = ({ src, alt, caption, onLoad, onSize, onClick }) => {
55
const [imageLoading, setImageLoading] = useState(true);
66
const [dimensions, setDimensions] = useState(null);
77

8+
// Check if it's a remote image
9+
const isRemoteImage = src.startsWith('http');
10+
811
const aspectRatio = dimensions?.width && dimensions?.height
912
? `${dimensions.width}/${dimensions.height}`
1013
: '4/3';
@@ -23,10 +26,22 @@ const Photo = ({ src, alt, caption, onLoad, onSize }) => {
2326
console.error('Error loading image:', src);
2427
};
2528

29+
const handleClick = () => {
30+
if (!imageLoading && onClick) {
31+
onClick();
32+
}
33+
};
34+
35+
// Add size parameters for Unsplash images to optimize loading
36+
const optimizedSrc = isRemoteImage && src.includes('unsplash.com')
37+
? `${src}?w=800&q=80&auto=format`
38+
: src;
39+
2640
return (
2741
<figure
28-
className={`photo-wrapper ${!imageLoading ? 'loaded' : ''}`}
42+
className={`photo-wrapper ${!imageLoading ? 'loaded' : ''} ${isRemoteImage ? 'remote-image' : ''}`}
2943
style={{ aspectRatio }}
44+
onClick={handleClick}
3045
>
3146
{imageLoading && (
3247
<div className="photo-loading" style={{ aspectRatio }}>
@@ -35,10 +50,13 @@ const Photo = ({ src, alt, caption, onLoad, onSize }) => {
3550
)}
3651
<img
3752
className={`lazyload photo-img ${!imageLoading ? 'loaded' : ''}`}
38-
data-src={src}
53+
data-src={optimizedSrc}
3954
alt={alt || 'Photo'}
4055
onLoad={handleLoad}
4156
onError={handleError}
57+
referrerPolicy={isRemoteImage ? "no-referrer" : ""}
58+
loading="lazy"
59+
decoding="async"
4260
/>
4361
{caption && (
4462
<figcaption className="photo-caption">{caption}</figcaption>
@@ -53,6 +71,7 @@ Photo.propTypes = {
5371
caption: PropTypes.string,
5472
onLoad: PropTypes.func,
5573
onSize: PropTypes.func,
74+
onClick: PropTypes.func
5675
};
5776

5877
export default Photo;

src/components/PhotoSidebar.astro

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,28 @@ const randomPicks = [
1818
{ title: "Best Moments", group: "best" },
1919
{ title: "Unexpected Shots", group: "unexpected" },
2020
];
21+
22+
const collections = [
23+
{ title: "Lotus Series", group: "lotus" },
24+
{ title: "Rose Collection", group: "rye" },
25+
{ title: "Nature Landscapes (Online)", group: "remote" },
26+
{ title: "Urban Scenes (Online)", group: "city" }
27+
];
2128
---
2229

2330
<div class="photo-sidebar-container">
24-
<h1 class="photos-title">My Photo Collection</h1>
31+
<div class="avatar-container">
32+
<img src="/my_avatar.png" alt="User Avatar" class="sidebar-avatar" />
33+
</div>
34+
35+
<div class="sidebar-section">
36+
<h3 class="section-title">My Collections</h3>
37+
<ul class="sidebar-list">
38+
{collections.map(item => (
39+
<SidebarItem {...item} />
40+
))}
41+
</ul>
42+
</div>
2543

2644
<div class="sidebar-section">
2745
<h3 class="section-title">Locations</h3>
@@ -46,26 +64,28 @@ const randomPicks = [
4664
<script>
4765
import { setPhotoGroup } from '../stores/photoStore';
4866

49-
// 处理点击事件
67+
// Handle click events
5068
document.querySelectorAll('a[data-group]').forEach(link => {
5169
link.addEventListener('click', (e) => {
5270
e.preventDefault();
53-
const group = e.currentTarget.dataset.group;
54-
// 移除其他active类
71+
const target = e.currentTarget as HTMLElement;
72+
const group = target.dataset.group;
73+
// Remove other active classes
5574
document.querySelectorAll('a[data-group]').forEach(a => a.classList.remove('active'));
56-
// 添加active类到当前项
57-
e.currentTarget.classList.add('active');
75+
// Add active class to current item
76+
target.classList.add('active');
5877
setPhotoGroup(group || null);
5978
});
6079
});
6180

62-
// 处理展开/折叠
81+
// Handle expand/collapse
6382
document.querySelectorAll('.location-toggle').forEach(toggle => {
6483
toggle.addEventListener('click', (e) => {
6584
e.preventDefault();
66-
const li = e.currentTarget.parentElement;
85+
const target = e.currentTarget as HTMLElement;
86+
const li = target.parentElement as HTMLElement;
6787
li.classList.toggle('expanded');
68-
e.currentTarget.classList.toggle('expanded');
88+
target.classList.toggle('expanded');
6989
});
7090
});
7191
</script>

0 commit comments

Comments
 (0)