Skip to content

Commit 3461337

Browse files
Spectualclaude
andcommitted
功能增强:BrowserRouter、GitHub动态、项目筛选、strictNullChecks
- strictNullChecks: true — tsconfig.app.json & tsconfig.json - HashRouter → BrowserRouter,添加 public/404.html + index.html 路径还原脚本(GitHub Pages SPA 方案) - ProfileSection:新增 GitHubActivity 组件,拉取 GitHub 公开 events API,展示最近 push 记录,加载态 + 优雅降级 - Projects:顶部加技术栈 tag 筛选器,终端 grep 风格命令行,匹配 tag 高亮,点击切换过滤 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7ba8436 commit 3461337

File tree

7 files changed

+184
-11
lines changed

7 files changed

+184
-11
lines changed

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@
5151
"knowsAbout": ["Machine Learning", "Computer Vision", "LLM", "RAG", "PyTorch", "Python"]
5252
}
5353
</script>
54+
<!-- GitHub Pages SPA: restore path from redirect query params set by 404.html -->
55+
<script>
56+
(function () {
57+
var params = new URLSearchParams(window.location.search);
58+
var p = params.get("p");
59+
if (p !== null) {
60+
var q = params.get("q");
61+
var url = "/" + decodeURIComponent(p) + (q ? "?" + decodeURIComponent(q) : "") + window.location.hash;
62+
window.history.replaceState(null, "", url);
63+
}
64+
})();
65+
</script>
5466
</head>
5567
<body>
5668
<div id="root"></div>

public/404.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Yifei Bao · AI/ML Engineer</title>
6+
<script>
7+
// GitHub Pages SPA redirect:
8+
// Convert /path?query#hash → /?p=path&q=query#hash
9+
// so index.html can restore the original URL via history.replaceState.
10+
(function () {
11+
var l = window.location;
12+
var path = l.pathname.slice(1); // strip leading "/"
13+
var redirect =
14+
l.protocol +
15+
"//" +
16+
l.host +
17+
"/?p=" +
18+
encodeURIComponent(path) +
19+
(l.search ? "&q=" + encodeURIComponent(l.search.slice(1)) : "") +
20+
l.hash;
21+
l.replace(redirect);
22+
})();
23+
</script>
24+
</head>
25+
<body></body>
26+
</html>

src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Toaster } from "@/components/ui/toaster";
33
import { Toaster as Sonner } from "@/components/ui/sonner";
44
import { TooltipProvider } from "@/components/ui/tooltip";
55
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6-
import { HashRouter, Routes, Route } from "react-router-dom";
6+
import { BrowserRouter, Routes, Route } from "react-router-dom";
77
import Index from "./pages/Index";
88
import NotFound from "./pages/NotFound";
99

@@ -59,7 +59,7 @@ const App = () => (
5959
<TooltipProvider>
6060
<Toaster />
6161
<Sonner />
62-
<HashRouter>
62+
<BrowserRouter>
6363
<Suspense fallback={null}>
6464
<Routes>
6565
<Route path="/" element={<Index />} />
@@ -68,7 +68,7 @@ const App = () => (
6868
<Route path="*" element={<NotFound />} />
6969
</Routes>
7070
</Suspense>
71-
</HashRouter>
71+
</BrowserRouter>
7272
</TooltipProvider>
7373
</QueryClientProvider>
7474
</ErrorBoundary>

src/components/ProfileSection.tsx

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef } from "react";
1+
import { useState, useEffect, useRef, memo } from "react";
22
import { personalInfo } from "@/data/personalInfo";
33
import { useTypewriter } from "@/hooks/useTypewriter";
44

@@ -143,6 +143,86 @@ const AutoTypeCLI = () => {
143143
);
144144
};
145145

146+
// ── GitHub Activity Feed ───────────────────────────────────────────────────
147+
interface GitHubPushEvent {
148+
type: string;
149+
repo: { name: string };
150+
created_at: string;
151+
payload: {
152+
commits?: { message: string; sha: string }[];
153+
ref?: string;
154+
};
155+
}
156+
157+
const GitHubActivity = memo(() => {
158+
const [events, setEvents] = useState<GitHubPushEvent[]>([]);
159+
const [status, setStatus] = useState<"loading" | "ok" | "error">("loading");
160+
161+
useEffect(() => {
162+
fetch("https://api.github.com/users/Spectual/events?per_page=30")
163+
.then((r) => (r.ok ? r.json() : Promise.reject()))
164+
.then((data: unknown[]) => {
165+
const pushes = (data as GitHubPushEvent[])
166+
.filter((e) => e.type === "PushEvent" && e.payload.commits?.length)
167+
.slice(0, 4);
168+
setEvents(pushes);
169+
setStatus("ok");
170+
})
171+
.catch(() => setStatus("error"));
172+
}, []);
173+
174+
if (status === "error" || (status === "ok" && events.length === 0)) return null;
175+
176+
return (
177+
<div
178+
style={{
179+
marginTop: "16px",
180+
paddingTop: "14px",
181+
borderTop: "1px solid var(--term-border)",
182+
fontSize: "12px",
183+
}}
184+
>
185+
<div style={{ color: "var(--term-dim)", marginBottom: "8px", fontSize: "11px" }}>
186+
# recent github activity
187+
</div>
188+
189+
{status === "loading" ? (
190+
<div style={{ color: "var(--term-dim)", fontSize: "11px" }}>
191+
<span style={{ color: "var(--term-green)" }}>$</span> git log --oneline
192+
<span className="cursor-blink" style={{ marginLeft: "4px" }} />
193+
</div>
194+
) : (
195+
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
196+
{events.map((ev, i) => {
197+
const repo = ev.repo.name.replace("spectual/", "");
198+
const branch = ev.payload.ref?.replace("refs/heads/", "") ?? "main";
199+
const commit = ev.payload.commits?.[0];
200+
const msg = commit?.message.split("\n")[0].slice(0, 60) ?? "";
201+
const sha = commit?.sha.slice(0, 7) ?? "";
202+
const date = new Date(ev.created_at).toLocaleDateString("en-US", {
203+
month: "short",
204+
day: "numeric",
205+
});
206+
return (
207+
<div key={i} style={{ display: "flex", gap: "6px", alignItems: "flex-start", flexWrap: "wrap" }}>
208+
<span style={{ color: "var(--term-dim)", fontSize: "10px", flexShrink: 0, marginTop: "1px" }}>
209+
{date}
210+
</span>
211+
<span style={{ color: "var(--term-green)", flexShrink: 0 }}>push</span>
212+
<span style={{ color: "var(--term-blue)", flexShrink: 0 }}>{repo}</span>
213+
<span style={{ color: "var(--term-dim)", flexShrink: 0 }}>({branch})</span>
214+
<span style={{ color: "var(--term-yellow)", fontSize: "10px", flexShrink: 0 }}>[{sha}]</span>
215+
<span style={{ color: "var(--term-text)", fontSize: "11px", wordBreak: "break-word" }}>{msg}</span>
216+
</div>
217+
);
218+
})}
219+
</div>
220+
)}
221+
</div>
222+
);
223+
});
224+
GitHubActivity.displayName = "GitHubActivity";
225+
146226
// ── ProfileSection ─────────────────────────────────────────────────────────
147227
const ProfileSection = () => {
148228
const skills = personalInfo.skills;
@@ -419,6 +499,9 @@ const ProfileSection = () => {
419499

420500
{/* Auto-type CLI */}
421501
<AutoTypeCLI />
502+
503+
{/* GitHub activity feed */}
504+
<GitHubActivity />
422505
</div>
423506
</div>
424507
</div>

src/pages/Projects.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
import { useState } from "react";
12
import { personalInfo } from "@/data/personalInfo";
23
import MatrixRain from "@/components/MatrixRain";
34
import TerminalFooter from "@/components/TerminalFooter";
45
import TerminalHeader from "@/components/TerminalHeader";
56

7+
const ALL_TECHS = [
8+
...new Set(personalInfo.projects.flatMap((p) => p.technologies)),
9+
];
10+
611
const Projects = () => {
12+
const [activeTech, setActiveTech] = useState<string | null>(null);
13+
14+
const filteredProjects = activeTech
15+
? personalInfo.projects.filter((p) => p.technologies.includes(activeTech))
16+
: personalInfo.projects;
717

818
return (
919
<div
@@ -36,22 +46,63 @@ const Projects = () => {
3646

3747
<div style={{ padding: "20px 24px" }}>
3848
{/* Command header */}
39-
<div style={{ marginBottom: "20px", fontSize: "13px" }}>
49+
<div style={{ marginBottom: "16px", fontSize: "13px" }}>
4050
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "4px" }}>
4151
<span style={{ color: "var(--term-green)" }}>spectual</span>
4252
<span style={{ color: "var(--term-dim)" }}>@</span>
4353
<span style={{ color: "var(--term-blue)" }}>github.io</span>
4454
<span style={{ color: "var(--term-text)" }}>:~$</span>
45-
<span style={{ color: "var(--term-text)", marginLeft: "4px" }}>ls -la ~/projects</span>
55+
<span style={{ color: "var(--term-text)", marginLeft: "4px" }}>
56+
{activeTech
57+
? `grep -i "${activeTech}" projects/`
58+
: "ls -la ~/projects"}
59+
</span>
4660
</div>
4761
<div style={{ color: "var(--term-dim)", fontSize: "11px" }}>
48-
total {personalInfo.projects.length} entries
62+
{filteredProjects.length === personalInfo.projects.length
63+
? `total ${personalInfo.projects.length} entries`
64+
: `${filteredProjects.length} match${filteredProjects.length !== 1 ? "es" : ""} (${personalInfo.projects.length} total)`}
65+
</div>
66+
</div>
67+
68+
{/* Tech stack filter */}
69+
<div
70+
style={{
71+
marginBottom: "20px",
72+
padding: "10px 12px",
73+
border: "1px solid var(--term-border)",
74+
backgroundColor: "var(--term-bg2)",
75+
fontSize: "11px",
76+
}}
77+
>
78+
<div style={{ color: "var(--term-dim)", marginBottom: "8px" }}>
79+
# filter by tech — click to toggle
80+
</div>
81+
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px" }}>
82+
{ALL_TECHS.map((tech) => (
83+
<button
84+
key={tech}
85+
onClick={() => setActiveTech(activeTech === tech ? null : tech)}
86+
style={{
87+
border: `1px solid ${activeTech === tech ? "var(--term-green)" : "var(--term-border)"}`,
88+
backgroundColor: activeTech === tech ? "rgba(57,255,20,0.08)" : "transparent",
89+
color: activeTech === tech ? "var(--term-green)" : "var(--term-dim)",
90+
fontSize: "10px",
91+
padding: "2px 8px",
92+
cursor: "pointer",
93+
fontFamily: "inherit",
94+
transition: "all 0.15s",
95+
}}
96+
>
97+
{tech}
98+
</button>
99+
))}
49100
</div>
50101
</div>
51102

52103
{/* Projects grid */}
53104
<div className="grid gap-4 lg:grid-cols-2">
54-
{personalInfo.projects.map((project, index) => {
105+
{filteredProjects.map((project, index) => {
55106
const isPatent = project.name.includes("Patent");
56107
const hasGithub = "githubUrl" in project && project.githubUrl;
57108
const hasLive = "liveUrl" in project && project.liveUrl;
@@ -156,8 +207,8 @@ const Projects = () => {
156207
<span
157208
key={tech}
158209
style={{
159-
border: "1px solid var(--term-border)",
160-
color: "var(--term-dim)",
210+
border: `1px solid ${activeTech === tech ? "var(--term-green)" : "var(--term-border)"}`,
211+
color: activeTech === tech ? "var(--term-green)" : "var(--term-dim)",
161212
fontSize: "10px",
162213
padding: "1px 6px",
163214
fontFamily: "inherit",

tsconfig.app.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
/* Linting */
1818
"strict": false,
19+
"strictNullChecks": true,
1920
"noUnusedLocals": false,
2021
"noUnusedParameters": false,
2122
"noImplicitAny": false,

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
"skipLibCheck": true,
1515
"allowJs": true,
1616
"noUnusedLocals": false,
17-
"strictNullChecks": false
17+
"strictNullChecks": true
1818
}
1919
}

0 commit comments

Comments
 (0)