feat: add all app components (AboutMe, Projects, Links, Alfred, Chengyu, Poker, Trash, Terminal)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
M1ngdaXie
2026-03-26 14:34:32 -07:00
parent 42bc84b278
commit 4425047f8d
8 changed files with 381 additions and 0 deletions

36
src/apps/AboutMe.tsx Normal file
View File

@@ -0,0 +1,36 @@
export default function AboutMe() {
return (
<div style={{ padding: '24px', color: '#fff', fontFamily: 'var(--font-ui)' }}>
<div style={{ display: 'flex', gap: 20, alignItems: 'flex-start', marginBottom: 20 }}>
<div style={{
width: 72, height: 72, borderRadius: '50%',
background: 'linear-gradient(135deg, #0a84ff, #bf5af2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 36, flexShrink: 0
}}>👤</div>
<div>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 4 }}>Mingda Xie <span style={{ color: 'rgba(255,255,255,0.4)', fontWeight: 400, fontSize: 14 }}></span></h2>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', lineHeight: 1.5 }}>Backend Developer · CS Master's @ Northeastern University</p>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)' }}>Shenzhen-bound · Class of 2026</p>
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 20 }}>
{['Java/Spring Cloud', 'Go', 'TypeScript/Bun', 'Redis', 'k3s', 'Microservices'].map(tag => (
<span key={tag} style={{
padding: '3px 10px', borderRadius: 20,
background: 'rgba(10, 132, 255, 0.15)',
border: '1px solid rgba(10,132,255,0.3)',
fontSize: 12, color: '#64d2ff'
}}>{tag}</span>
))}
</div>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)', lineHeight: 1.7, marginBottom: 16 }}>
Backend engineer who runs a personal k3s cluster on Vultr, ships microservices, and builds things that occasionally work in production.
LeetCode 400+. Pixel art enjoyer. Stardew Valley veteran. Huge fan of 土屋アンナ.
</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>
"I am the dragon scroll, bitch." Genuine mastery over shortcuts.
</p>
</div>
);
}

28
src/apps/Alfred.tsx Normal file
View File

@@ -0,0 +1,28 @@
export default function Alfred() {
return (
<div style={{ padding: '28px 24px', color: '#fff', fontFamily: 'var(--font-ui)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<div style={{
width: 56, height: 56, borderRadius: 14,
background: 'linear-gradient(135deg, #ff453a, #bf5af2)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28
}}></div>
<div>
<h2 style={{ fontSize: 18, fontWeight: 600 }}>Alfred</h2>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.5)' }}>Personal AI Agent</p>
</div>
</div>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)', lineHeight: 1.7, marginBottom: 16 }}>
Alfred is my self-hosted AI agent running on k3s. It handles task management, reminders,
Telegram notifications, and acts as a personal ops layer for my homelab stack.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{['Task & reminder management', 'Telegram bot interface', 'Home automation bridge', 'LLM-powered command routing'].map(f => (
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'rgba(255,255,255,0.6)' }}>
<span style={{ color: '#30d158', fontSize: 10 }}></span> {f}
</div>
))}
</div>
</div>
);
}

32
src/apps/Chengyu.tsx Normal file
View File

@@ -0,0 +1,32 @@
export default function Chengyu() {
return (
<div style={{ padding: '28px 24px', color: '#fff', textAlign: 'center' }}>
<div style={{ fontSize: 56, marginBottom: 16 }}>🀄</div>
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 8 }}></h2>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)', marginBottom: 4 }}>Chinese Idiom Guessing Game</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.35)', marginBottom: 24 }}>Wordle-style · Daily puzzle · 4-character idioms</p>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.65)', lineHeight: 1.7, marginBottom: 28 }}>
Guess the 4-character Chinese idiom () in 6 tries.
Correct characters turn green, misplaced turn yellow.
A new puzzle drops every day at midnight.
</p>
<a
href="https://chengyu.m1ngdaxie.com"
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-block',
padding: '10px 28px',
borderRadius: 20,
background: 'linear-gradient(135deg, #ffd60a, #ff453a)',
color: '#000',
fontWeight: 600,
fontSize: 14,
textDecoration: 'none',
}}
>
Launch Game
</a>
</div>
);
}

41
src/apps/Links.tsx Normal file
View File

@@ -0,0 +1,41 @@
const LINKS = [
{ label: 'GitHub', sub: 'github.com/m1ngdaxie', emoji: '🐙', url: 'https://github.com/m1ngdaxie', color: '#30d158' },
{ label: 'Homepage', sub: 'm1ngdaxie.com', emoji: '🌐', url: 'https://m1ngdaxie.com', color: '#0a84ff' },
{ label: '成语填空', sub: 'chengyu.m1ngdaxie.com', emoji: '🀄', url: 'https://chengyu.m1ngdaxie.com', color: '#ff453a' },
{ label: 'Email', sub: 'mingda@m1ngdaxie.com', emoji: '✉️', url: 'mailto:mingda@m1ngdaxie.com', color: '#bf5af2' },
];
export default function Links() {
return (
<div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{LINKS.map(l => (
<a
key={l.label}
href={l.url}
target="_blank"
rel="noopener noreferrer"
style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', borderRadius: 10, textDecoration: 'none',
background: 'rgba(255,255,255,0.04)',
border: `1px solid rgba(255,255,255,0.07)`,
transition: 'background 0.15s',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(255,255,255,0.08)')}
onMouseLeave={e => (e.currentTarget.style.background = 'rgba(255,255,255,0.04)')}
>
<div style={{
width: 40, height: 40, borderRadius: 10,
background: `${l.color}22`, border: `1px solid ${l.color}44`,
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20
}}>{l.emoji}</div>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: '#fff' }}>{l.label}</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.4)' }}>{l.sub}</div>
</div>
<div style={{ marginLeft: 'auto', color: 'rgba(255,255,255,0.25)', fontSize: 14 }}></div>
</a>
))}
</div>
);
}

16
src/apps/Poker.tsx Normal file
View File

@@ -0,0 +1,16 @@
export default function Poker() {
return (
<div style={{ padding: 40, color: '#fff', textAlign: 'center' }}>
<div style={{ fontSize: 64, marginBottom: 16 }}>🎲</div>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Poker</h2>
<div style={{
display: 'inline-block', padding: '6px 16px', borderRadius: 20,
background: 'rgba(255,214,10,0.12)', border: '1px solid rgba(255,214,10,0.3)',
color: '#ffd60a', fontSize: 12, marginBottom: 16
}}>🚧 Under Construction</div>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.4)', lineHeight: 1.6 }}>
A Texas Hold'em practice tool is in the works.<br />Check back later.
</p>
</div>
);
}

38
src/apps/Projects.tsx Normal file
View File

@@ -0,0 +1,38 @@
const PROJECTS = [
{ name: 'CampusNest', desc: 'Campus housing platform with Spring Cloud microservices, Redis caching, and k3s deployment.', tags: ['Java', 'Spring Cloud', 'Redis', 'k3s'], color: '#0a84ff' },
{ name: 'Alfred Bot', desc: 'Personal AI agent — task management, reminders, home automation bridge.', tags: ['Go', 'TypeScript', 'Bun'], color: '#bf5af2' },
{ name: 'Warden / Bastion', desc: 'Self-hosted auth and proxy layer running on k3s with Let\'s Encrypt.', tags: ['Nginx', 'k3s', 'VLESS'], color: '#30d158' },
{ name: 'Collab Platform', desc: 'Real-time collaborative workspace with WebSocket rooms and conflict resolution.', tags: ['TypeScript', 'Bun', 'WebSocket'], color: '#ffd60a' },
{ name: '成语填空', desc: 'Daily Chinese idiom guessing game — Wordle-style, runs at chengyu.m1ngdaxie.com.', tags: ['React', 'TypeScript'], color: '#ff453a' },
];
export default function Projects() {
return (
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{PROJECTS.map(p => (
<div key={p.name} style={{
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.07)',
borderRadius: 10,
padding: '14px 16px',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: p.color, flexShrink: 0 }} />
<span style={{ fontWeight: 600, fontSize: 14, color: '#fff' }}>{p.name}</span>
</div>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', lineHeight: 1.5, marginBottom: 8 }}>{p.desc}</p>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{p.tags.map(t => (
<span key={t} style={{
fontSize: 11, padding: '2px 8px', borderRadius: 12,
background: 'rgba(255,255,255,0.06)',
color: 'rgba(255,255,255,0.5)',
border: '1px solid rgba(255,255,255,0.1)'
}}>{t}</span>
))}
</div>
</div>
))}
</div>
);
}

178
src/apps/Terminal.tsx Normal file
View File

@@ -0,0 +1,178 @@
import { useState, useRef, useEffect } from 'react';
import type { KeyboardEvent } from 'react';
interface Line { type: 'input' | 'output'; text: string; key: number }
const COMMANDS: Record<string, string[]> = {
help: [
'Available commands:',
' help — show this list',
' about — who is mingda',
' projects — list projects',
' skills — tech stack',
' contact — contact info',
' neofetch — system info',
' clear — clear terminal',
],
about: [
'Mingda Xie (解明达)',
'Backend Developer · CS Master\'s @ Northeastern University',
'Shenzhen-bound · Class of 2026',
'',
'Runs a personal k3s cluster. Writes Go and Java at 2am.',
'LeetCode 400+. Pixel art. Stardew Valley. 土屋アンナ.',
'',
'"I am the dragon scroll, bitch."',
],
projects: [
'Projects:',
' CampusNest — Spring Cloud housing platform',
' Alfred Bot — Self-hosted AI agent',
' Warden/Bastion — k3s auth + proxy layer',
' Collab Platform— Real-time collaborative workspace',
' 成语填空 — Daily Chinese idiom game',
],
skills: [
'Tech Stack:',
' Languages : Java, Go, TypeScript, Python',
' Frameworks : Spring Cloud, Bun, React',
' Infra : k3s, Nginx, Redis, Docker',
' Cloud : Vultr, Let\'s Encrypt',
' Other : Microservices, WebSocket, LLM APIs',
],
contact: [
'Contact:',
' GitHub : github.com/m1ngdaxie',
' Web : m1ngdaxie.com',
' Game : chengyu.m1ngdaxie.com',
' Email : mingda@m1ngdaxie.com',
],
neofetch: [
' . mingda@doitou-sv',
' .:. ----------------',
' .:!:. OS: MingdaOS 1.0',
' .:!!!:. Host: Vultr VPS',
' .::!!!::. Kernel: Ubuntu 24.04',
' .:::!!!:::. RAM: 4GB',
' .::::!!!::::. Shell: zsh',
' .:::::!!!:::::. Services: Alfred, Chengyu, k3s, VLESS',
'',
' nginx + k3s + let\'s encrypt',
],
};
const PROMPT = 'mingda@doitou-sv ~ % ';
let lineCounter = 0; // module-level counter for stable unique keys
function nextKey() { return ++lineCounter; }
export default function Terminal() {
const [lines, setLines] = useState<Line[]>([
{ type: 'output', text: 'MingdaOS Terminal v1.0', key: nextKey() },
{ type: 'output', text: 'Type "help" to see available commands.', key: nextKey() },
{ type: 'output', text: '', key: nextKey() },
]);
const [input, setInput] = useState('');
const [history, setHistory] = useState<string[]>([]);
const [histIdx, setHistIdx] = useState(-1);
const bottomRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [lines]);
useEffect(() => {
inputRef.current?.focus();
}, []);
function handleSubmit() {
const cmd = input.trim().toLowerCase();
const newLines: Line[] = [...lines, { type: 'input', text: PROMPT + input, key: nextKey() }];
if (cmd === '') {
setLines([...newLines, { type: 'output', text: '', key: nextKey() }]);
} else if (cmd === 'clear') {
setLines([]);
} else if (COMMANDS[cmd]) {
COMMANDS[cmd].forEach(l => newLines.push({ type: 'output', text: l, key: nextKey() }));
newLines.push({ type: 'output', text: '', key: nextKey() });
setLines(newLines);
} else {
newLines.push({ type: 'output', text: `zsh: command not found: ${cmd}`, key: nextKey() });
newLines.push({ type: 'output', text: '', key: nextKey() });
setLines(newLines);
}
if (cmd) setHistory(h => [cmd, ...h].slice(0, 50));
setHistIdx(-1);
setInput('');
}
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
handleSubmit();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const next = Math.min(histIdx + 1, history.length - 1);
setHistIdx(next);
if (history[next] !== undefined) setInput(history[next]);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
const next = Math.max(histIdx - 1, -1);
setHistIdx(next);
setInput(next === -1 ? '' : history[next]);
}
}
return (
<div
style={{
fontFamily: 'var(--font-mono)',
fontSize: 13,
color: '#e2e8f0',
background: 'transparent',
padding: '12px 16px',
height: '100%',
display: 'flex',
flexDirection: 'column',
cursor: 'text',
}}
onClick={() => inputRef.current?.focus()}
>
<div style={{ flex: 1, overflowY: 'auto' }}>
{lines.map((line) => (
<div key={line.key} style={{
color: line.type === 'input' ? '#64d2ff' : '#e2e8f0',
whiteSpace: 'pre',
lineHeight: 1.6,
}}>
{line.text}
</div>
))}
<div ref={bottomRef} />
</div>
<div style={{ display: 'flex', alignItems: 'center', marginTop: 4 }}>
<span style={{ color: '#64d2ff', whiteSpace: 'nowrap' }}>{PROMPT}</span>
<input
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
style={{
flex: 1,
background: 'transparent',
border: 'none',
outline: 'none',
color: '#e2e8f0',
fontFamily: 'var(--font-mono)',
fontSize: 13,
caretColor: '#64d2ff',
}}
spellCheck={false}
autoComplete="off"
/>
</div>
</div>
);
}

12
src/apps/Trash.tsx Normal file
View File

@@ -0,0 +1,12 @@
export default function Trash() {
return (
<div style={{ padding: 40, color: '#fff', textAlign: 'center' }}>
<div style={{ fontSize: 64, marginBottom: 16 }}>🗑</div>
<h2 style={{ fontSize: 18, fontWeight: 600, marginBottom: 8 }}>Trash</h2>
<p style={{ fontSize: 13, color: 'rgba(255,255,255,0.4)', marginBottom: 20 }}>The trash is empty.</p>
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.2)', fontStyle: 'italic' }}>
just like my social life
</p>
</div>
);
}