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:
36
src/apps/AboutMe.tsx
Normal file
36
src/apps/AboutMe.tsx
Normal 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
28
src/apps/Alfred.tsx
Normal 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
32
src/apps/Chengyu.tsx
Normal 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
41
src/apps/Links.tsx
Normal 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
16
src/apps/Poker.tsx
Normal 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
38
src/apps/Projects.tsx
Normal 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
178
src/apps/Terminal.tsx
Normal 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
12
src/apps/Trash.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user