Skip to content

Commit 63130aa

Browse files
committed
合并代码质量优化:TerminalHeader、类型统一、无障碍修复等
2 parents 3a8e289 + f738061 commit 63130aa

File tree

12 files changed

+146
-233
lines changed

12 files changed

+146
-233
lines changed

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
<meta property="og:type" content="profile" />
1313
<meta property="og:title" content="Yifei Bao · AI/ML Engineer" />
1414
<meta property="og:description" content="Chat with an AI to learn about Yifei's background, projects, and experience." />
15-
<meta property="og:image" content="/og-image.webp" />
15+
<meta property="og:image" content="https://spectual.github.io/og-image.webp" />
1616
<meta property="og:url" content="https://spectual.github.io" />
1717

1818
<!-- Twitter -->
1919
<meta name="twitter:card" content="summary_large_image" />
2020
<meta name="twitter:title" content="Yifei Bao · AI/ML Engineer" />
2121
<meta name="twitter:description" content="Chat with an AI to learn about Yifei's background, projects, and experience." />
22-
<meta name="twitter:image" content="/og-image.webp" />
22+
<meta name="twitter:image" content="https://spectual.github.io/og-image.webp" />
2323

2424
<!-- Schema.org Person structured data -->
2525
<script type="application/ld+json">

src/components/ChatSection.tsx

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@ import { AlertCircle } from "lucide-react";
33
import MessageBubble from "./MessageBubble";
44
import { sendMessage, checkHealth } from "@/utils/api";
55
import { toast } from "sonner";
6-
7-
interface Message {
8-
id: string;
9-
text: string;
10-
isUser: boolean;
11-
timestamp: Date;
12-
}
6+
import type { Message } from "@/types/chat";
137

148
const PREDEFINED_QUESTIONS = [
159
"What is your background?",
@@ -260,6 +254,8 @@ const ChatSection = () => {
260254
{/* Messages area */}
261255
<div
262256
ref={scrollContainerRef}
257+
role="log"
258+
aria-live="polite"
263259
className="terminal-scroll"
264260
style={{
265261
height: "min(384px, 50dvh)",
@@ -301,6 +297,7 @@ const ChatSection = () => {
301297
key={index}
302298
onClick={() => handleSendMessage(question)}
303299
disabled={isLoading || !isServerOnline}
300+
className="chat-quick-btn"
304301
style={{
305302
padding: "2px 8px",
306303
border: "1px solid var(--term-border)",
@@ -312,18 +309,6 @@ const ChatSection = () => {
312309
transition: "all 0.15s",
313310
opacity: isLoading || !isServerOnline ? 0.4 : 1,
314311
}}
315-
onMouseEnter={(e) => {
316-
if (!isLoading && isServerOnline) {
317-
e.currentTarget.style.borderColor = "var(--term-green)";
318-
e.currentTarget.style.color = "var(--term-green)";
319-
e.currentTarget.style.backgroundColor = "rgba(63,185,80,0.06)";
320-
}
321-
}}
322-
onMouseLeave={(e) => {
323-
e.currentTarget.style.borderColor = "var(--term-border)";
324-
e.currentTarget.style.color = "var(--term-dim)";
325-
e.currentTarget.style.backgroundColor = "transparent";
326-
}}
327312
>
328313
[{index + 1}] {question}
329314
</button>

src/components/GetInTouchSection.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from "react";
22
import { toast } from "@/components/ui/use-toast";
3+
import { personalInfo } from "@/data/personalInfo";
34

45
const GetInTouchSection = () => {
56
const [form, setForm] = useState({ name: "", company: "", email: "", message: "" });
@@ -43,7 +44,7 @@ const GetInTouchSection = () => {
4344
form.message,
4445
].join("\n")
4546
);
46-
window.location.href = `mailto:baoyifei@bu.edu?subject=${subject}&body=${body}`;
47+
window.location.href = `mailto:${personalInfo.email}?subject=${subject}&body=${body}`;
4748
setSubmitting(false);
4849
};
4950

@@ -110,10 +111,11 @@ const GetInTouchSection = () => {
110111
<form onSubmit={handleSubmit}>
111112
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
112113
<div>
113-
<label style={labelStyle}>
114+
<label htmlFor="contact-name" style={labelStyle}>
114115
name <span style={{ color: "var(--term-red)" }}>*</span>
115116
</label>
116117
<input
118+
id="contact-name"
117119
placeholder="your name"
118120
value={form.name}
119121
onChange={(e) => setForm({ ...form, name: e.target.value })}
@@ -123,11 +125,12 @@ const GetInTouchSection = () => {
123125
/>
124126
</div>
125127
<div>
126-
<label style={labelStyle}>
128+
<label htmlFor="contact-company" style={labelStyle}>
127129
company{" "}
128130
<span style={{ color: "var(--term-dim)", fontSize: "10px" }}>(optional)</span>
129131
</label>
130132
<input
133+
id="contact-company"
131134
placeholder="company name"
132135
value={form.company}
133136
onChange={(e) => setForm({ ...form, company: e.target.value })}
@@ -139,10 +142,11 @@ const GetInTouchSection = () => {
139142
</div>
140143

141144
<div style={{ marginBottom: "12px" }}>
142-
<label style={labelStyle}>
145+
<label htmlFor="contact-email" style={labelStyle}>
143146
email <span style={{ color: "var(--term-red)" }}>*</span>
144147
</label>
145148
<input
149+
id="contact-email"
146150
type="email"
147151
placeholder="you@example.com"
148152
value={form.email}
@@ -154,10 +158,11 @@ const GetInTouchSection = () => {
154158
</div>
155159

156160
<div style={{ marginBottom: "16px" }}>
157-
<label style={labelStyle}>
161+
<label htmlFor="contact-message" style={labelStyle}>
158162
message <span style={{ color: "var(--term-red)" }}>*</span>
159163
</label>
160164
<textarea
165+
id="contact-message"
161166
placeholder="your message..."
162167
rows={5}
163168
value={form.message}
@@ -210,9 +215,9 @@ const GetInTouchSection = () => {
210215

211216
<div style={{ fontSize: "12px" }}>
212217
{[
213-
{ key: "email", value: "baoyifei@bu.edu", href: "mailto:baoyifei@bu.edu" },
214-
{ key: "phone", value: "+1 857 340 3064", href: "tel:+18573403064" },
215-
{ key: "location", value: "Boston, MA", href: null },
218+
{ key: "email", value: personalInfo.email, href: `mailto:${personalInfo.email}` },
219+
{ key: "phone", value: personalInfo.phone, href: `tel:${personalInfo.phone.replace(/\s/g, "")}` },
220+
{ key: "location", value: personalInfo.location, href: null },
216221
].map(({ key, value, href }) => (
217222
<div
218223
key={key}
@@ -231,9 +236,8 @@ const GetInTouchSection = () => {
231236
{href ? (
232237
<a
233238
href={href}
234-
style={{ color: "var(--term-text)", textDecoration: "none" }}
235-
onMouseEnter={(e) => (e.currentTarget.style.color = "var(--term-green)")}
236-
onMouseLeave={(e) => (e.currentTarget.style.color = "var(--term-text)")}
239+
className="contact-link"
240+
style={{ color: "var(--term-text)", textDecoration: "none", transition: "color 0.15s" }}
237241
>
238242
{value}
239243
</a>

src/components/MatrixRain.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@ const MatrixRain = () => {
1515
// Respect reduced motion preference
1616
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1717

18-
// Lower density on mobile
19-
const isMobile = window.innerWidth < 768;
20-
2118
const fontSize = 14;
19+
let isMobile = window.innerWidth < 768;
2220
let columns = 0;
2321
let drops: number[] = [];
2422

2523
const resize = () => {
2624
canvas.width = window.innerWidth;
2725
canvas.height = window.innerHeight;
26+
isMobile = window.innerWidth < 768;
2827
const newCols = isMobile
2928
? Math.floor(canvas.width / (fontSize * 3))
3029
: Math.floor(canvas.width / fontSize);

src/components/MessageBubble.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import { useEffect, useRef } from "react";
1+
import { useRef } from "react";
22
import ReactMarkdown from "react-markdown";
33
import { useTypewriter } from "@/hooks/useTypewriter";
4-
5-
interface Message {
6-
id: string;
7-
text: string;
8-
isUser: boolean;
9-
timestamp: Date;
10-
}
4+
import type { Message } from "@/types/chat";
115

126
interface MessageBubbleProps {
137
message: Message;
@@ -63,13 +57,7 @@ const MessageBubble = ({ message, isTyping = false, onTypingComplete }: MessageB
6357
{ speed: 10, onComplete: () => onCompleteRef.current?.() }
6458
);
6559

66-
// Scroll into view as text streams in
6760
const ref = useRef<HTMLDivElement>(null);
68-
useEffect(() => {
69-
if (isTyping && ref.current) {
70-
ref.current.scrollIntoView({ behavior: "smooth", block: "nearest" });
71-
}
72-
}, [displayed, isTyping]);
7361

7462
if (message.isUser) {
7563
return (

src/components/TerminalHeader.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Link, useLocation } from "react-router-dom";
2+
3+
const NAV_LINKS = [
4+
{ to: "/", label: "~/chat" },
5+
{ to: "/resume", label: "~/resume" },
6+
{ to: "/projects", label: "~/projects" },
7+
];
8+
9+
interface TerminalHeaderProps {
10+
title?: string;
11+
}
12+
13+
const TerminalHeader = ({ title }: TerminalHeaderProps) => {
14+
const location = useLocation();
15+
16+
return (
17+
<header
18+
aria-label={title}
19+
style={{
20+
borderBottom: "1px solid var(--term-border)",
21+
backgroundColor: "var(--term-bg2)",
22+
}}
23+
>
24+
<div className="max-w-4xl mx-auto px-4 py-0 flex items-stretch gap-0">
25+
{/* Window controls — hidden on mobile */}
26+
<div className="hidden sm:flex gap-1.5 items-center px-4 shrink-0">
27+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: "var(--term-red)" }} />
28+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: "var(--term-yellow)" }} />
29+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: "var(--term-green)" }} />
30+
</div>
31+
32+
{/* Tab bar */}
33+
<nav className="flex items-stretch flex-1">
34+
{NAV_LINKS.map(({ to, label }) => {
35+
const isActive = location.pathname === to;
36+
return (
37+
<Link
38+
key={to}
39+
to={to}
40+
className={`${isActive ? "nav-active" : "nav-tab"} flex items-center text-[11px] sm:text-[12px] px-2.5 sm:px-4 py-1.5 sm:py-2`}
41+
style={{
42+
color: isActive ? "var(--term-text)" : "var(--term-dim)",
43+
backgroundColor: isActive ? "var(--term-bg)" : "transparent",
44+
borderLeft: `1px solid ${isActive ? "var(--term-border)" : "transparent"}`,
45+
borderRight: `1px solid ${isActive ? "var(--term-border)" : "transparent"}`,
46+
borderTop: `2px solid ${isActive ? "var(--term-green)" : "transparent"}`,
47+
borderBottom: `1px solid ${isActive ? "var(--term-bg)" : "transparent"}`,
48+
textDecoration: "none",
49+
transition: "all 0.15s",
50+
marginBottom: isActive ? "-1px" : "0",
51+
}}
52+
>
53+
{label}
54+
</Link>
55+
);
56+
})}
57+
</nav>
58+
59+
{/* Shell info — hidden on mobile */}
60+
<div
61+
className="hidden sm:flex items-center px-4 shrink-0"
62+
style={{ fontSize: "11px", color: "var(--term-dim)" }}
63+
>
64+
spectual@github.io
65+
</div>
66+
</div>
67+
</header>
68+
);
69+
};
70+
71+
export default TerminalHeader;

src/data/personalInfo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const personalInfo = {
22
name: "Yifei Bao",
33
title: "AI / ML Engineer",
44
email: "baoyifei@bu.edu",
5+
phone: "+1 857 340 3064",
56
location: "Boston, MA",
67
avatar: "/avatar.webp",
78
social: {

src/index.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,40 @@
295295
);
296296
}
297297

298+
/* Project card hover */
299+
.project-card:hover {
300+
border-color: var(--term-green) !important;
301+
background-color: rgba(63, 185, 80, 0.03) !important;
302+
}
303+
304+
/* Project card link buttons */
305+
.project-link-github:hover {
306+
color: var(--term-blue) !important;
307+
border-color: var(--term-blue) !important;
308+
}
309+
.project-link-live:hover {
310+
color: var(--term-green) !important;
311+
border-color: var(--term-green) !important;
312+
}
313+
314+
/* GitHub profile link */
315+
.github-profile-link:hover {
316+
border-color: var(--term-blue) !important;
317+
background-color: rgba(88, 166, 255, 0.08) !important;
318+
}
319+
320+
/* Chat quick-question buttons */
321+
.chat-quick-btn:hover:not(:disabled) {
322+
border-color: var(--term-green) !important;
323+
color: var(--term-green) !important;
324+
background-color: rgba(63, 185, 80, 0.06) !important;
325+
}
326+
327+
/* Contact info links */
328+
.contact-link:hover {
329+
color: var(--term-green) !important;
330+
}
331+
298332
/* prefers-reduced-motion */
299333
@media (prefers-reduced-motion: reduce) {
300334
.cursor-blink,

0 commit comments

Comments
 (0)