feat: enhance user interface with CRT effects, update AboutMe and Projects sections, and add new Collab app
All checks were successful
Deploy / deploy (push) Successful in 14s
All checks were successful
Deploy / deploy (push) Successful in 14s
This commit is contained in:
40
src/App.css
40
src/App.css
@@ -37,3 +37,43 @@ html, body, #root {
|
||||
font-smooth: never;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* CRT scanlines + faint phosphor glow over the entire desktop */
|
||||
#root::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 255, 255, 0.025) 0px,
|
||||
rgba(0, 255, 255, 0.025) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
animation: crt-flicker 4.2s steps(60) infinite;
|
||||
}
|
||||
|
||||
@keyframes crt-flicker {
|
||||
0%, 100% { opacity: 0.85; }
|
||||
47% { opacity: 0.92; }
|
||||
50% { opacity: 0.6; }
|
||||
53% { opacity: 0.9; }
|
||||
}
|
||||
|
||||
@keyframes blink-caret {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.cypher-caret {
|
||||
display: inline-block;
|
||||
width: 0.55em;
|
||||
margin-left: 2px;
|
||||
background: var(--pixel-cyan);
|
||||
color: transparent;
|
||||
animation: blink-caret 1s steps(1) infinite;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,177 @@
|
||||
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
|
||||
style={{ padding: "24px", color: "#fff", fontFamily: "var(--font-ui)" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 12,
|
||||
color: "var(--pixel-cyan)",
|
||||
marginBottom: 16,
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
<span style={{ opacity: 0.5 }}>mingda@doitou-sv:~$ </span>
|
||||
whoami<span className="cypher-caret">█</span>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</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
|
||||
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: "2px 8px",
|
||||
borderRadius: 0,
|
||||
background: "rgba(0,255,255,0.06)",
|
||||
border: "1px solid rgba(0,255,255,0.25)",
|
||||
fontSize: 11,
|
||||
fontFamily: "var(--font-mono)",
|
||||
color: "var(--pixel-cyan)",
|
||||
letterSpacing: 0.3,
|
||||
}}
|
||||
>
|
||||
[{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.
|
||||
Pixel art enjoyer. Stardew Valley veteran. Huge fan of 土屋アンナ.
|
||||
<p
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontFamily: "var(--font-mono)",
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
lineHeight: 1.7,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "rgba(0,255,255,0.45)" }}>// </span>
|
||||
Backend engineer. Distributed systems, infra, and the long tail of
|
||||
things that go wrong at 3am. Trust the math, not the institution.
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>
|
||||
{/* <p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>
|
||||
"I am the dragon scroll, bitch." — Genuine mastery over shortcuts.
|
||||
</p>
|
||||
</p> */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: "12px 14px",
|
||||
background: "rgba(0,0,0,0.25)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
letterSpacing: 0.5,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Signed Identity
|
||||
</span>
|
||||
<a
|
||||
href="/pgp.asc"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "#64d2ff",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
pgp.asc ↗
|
||||
</a>
|
||||
</div>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 11,
|
||||
lineHeight: 1.5,
|
||||
color: "rgba(255,255,255,0.65)",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>{`-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
identity: Mingda Xie
|
||||
|
||||
email: xiemingda2020@gmail.com
|
||||
pgp: 8C50 AD5D CE0E 949E 39FA F19D 609C 704E 544A 0BFD
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEARYKADkWIQSNntaQmSEMuam1aJhRySHSOW66vwUCafyKpRsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwwLDMACgkQUckh0jluur8brQD/RzlXBuVuQnGioNsibNDm
|
||||
Rm85/5ed2BROS5gaH4FCBrQBALjMapIlyCYBNvIZNQX1eFCY/WNfZQXe3woeZYlP
|
||||
E/IF
|
||||
=k1lO
|
||||
-----END PGP SIGNATURE-----`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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' },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Line { type: 'input' | 'output'; text: string; key: number }
|
||||
|
||||
@@ -8,12 +8,34 @@ const COMMANDS: Record<string, string[]> = {
|
||||
'Available commands:',
|
||||
' help — show this list',
|
||||
' about — who is mingda',
|
||||
' whoami — current identity',
|
||||
' projects — list projects',
|
||||
' skills — tech stack',
|
||||
' contact — contact info',
|
||||
' pgp — public key fingerprint',
|
||||
' motd — message of the day',
|
||||
' neofetch — system info',
|
||||
' clear — clear terminal',
|
||||
],
|
||||
whoami: [
|
||||
'mingda',
|
||||
'uid=1000 groups=(wheel,crypto,sudoers)',
|
||||
'shell=/bin/zsh tty=/dev/pts/0',
|
||||
],
|
||||
pgp: [
|
||||
'PGP fingerprint:',
|
||||
' 8C50 AD5D CE0E 949E 39FA F19D 609C 704E 544A 0BFD',
|
||||
'',
|
||||
'Public key: /pgp.asc',
|
||||
'Verify before you trust.',
|
||||
],
|
||||
motd: [
|
||||
'cypherpunks write code.',
|
||||
'privacy is necessary for an open society in the electronic age.',
|
||||
'we cannot expect governments, corporations, or other large,',
|
||||
'faceless organizations to grant us privacy out of their beneficence.',
|
||||
' — eric hughes, 1993',
|
||||
],
|
||||
about: [
|
||||
'Mingda Xie (解明达)',
|
||||
'Backend Developer · CS Master\'s @ Northeastern University',
|
||||
@@ -28,7 +50,6 @@ const COMMANDS: Record<string, string[]> = {
|
||||
'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',
|
||||
],
|
||||
@@ -55,7 +76,7 @@ const COMMANDS: Record<string, string[]> = {
|
||||
' .::!!!::. Kernel: Ubuntu 24.04',
|
||||
' .:::!!!:::. RAM: 4GB',
|
||||
' .::::!!!::::. Shell: zsh',
|
||||
' .:::::!!!:::::. Services: Alfred, Chengyu, k3s, VLESS',
|
||||
' .:::::!!!:::::. Services: Alfred, Chengyu, k3s',
|
||||
'',
|
||||
' nginx + k3s + let\'s encrypt',
|
||||
],
|
||||
@@ -68,8 +89,9 @@ 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: 'MingdaOS Terminal v1.0 [secure tty]', key: nextKey() },
|
||||
{ type: 'output', text: 'kernel: linux 6.x | tls: 1.3 | pgp: 8C50AD5D...0BFD', key: nextKey() },
|
||||
{ type: 'output', text: 'cypherpunks write code. type "help" for commands.', key: nextKey() },
|
||||
{ type: 'output', text: '', key: nextKey() },
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
@@ -50,12 +50,19 @@ export default function Desktop() {
|
||||
gridRow: app.desktopPosition.row + 1,
|
||||
}}
|
||||
>
|
||||
<DesktopIcon app={app} onOpen={() => openWindow(app.id)} />
|
||||
<DesktopIcon
|
||||
app={app}
|
||||
onOpen={() =>
|
||||
app.externalUrl
|
||||
? window.open(app.externalUrl, '_blank', 'noopener,noreferrer')
|
||||
: openWindow(app.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{APPS.map(app => (
|
||||
{APPS.filter(app => !app.externalUrl).map(app => (
|
||||
<Window key={app.id} app={app} />
|
||||
))}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ export default function DesktopIcon({ app, onOpen }: Props) {
|
||||
return (
|
||||
<div className="desktop-icon" onDoubleClick={onOpen}>
|
||||
<div className="desktop-icon-img" style={{ background: app.iconGradient }}>
|
||||
<span>{app.emoji}</span>
|
||||
{app.iconImage
|
||||
? <img src={app.iconImage} alt={app.title} style={{ width: '70%', height: '70%', objectFit: 'contain' }} />
|
||||
: <span>{app.emoji}</span>}
|
||||
</div>
|
||||
<span className="desktop-icon-label">{app.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import './MenuBar.css';
|
||||
|
||||
const MENU_ITEMS = ['File', 'Edit', 'View', 'Go', 'Help'];
|
||||
const MENU_ITEMS = ['encrypt', 'sign', 'verify', 'wipe', 'about'];
|
||||
const BOOT_AT = Date.now();
|
||||
|
||||
export default function MenuBar() {
|
||||
const [time, setTime] = useState(() => formatTime(new Date()));
|
||||
const [uptime, setUptime] = useState(() => formatUptime(0));
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setTime(formatTime(new Date())), 1000);
|
||||
const id = setInterval(() => {
|
||||
setTime(formatTime(new Date()));
|
||||
setUptime(formatUptime(Date.now() - BOOT_AT));
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="menubar">
|
||||
<div className="menubar-left">
|
||||
<span className="menubar-logo"></span>
|
||||
<span className="menubar-logo">[ MingdaOS ]</span>
|
||||
{MENU_ITEMS.map(item => (
|
||||
<span key={item} className="menubar-item">{item}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="menubar-right">
|
||||
<span className="menubar-clock">{time}</span>
|
||||
<span className="menubar-clock" style={{ opacity: 0.55, marginRight: 12 }}>
|
||||
up {uptime}
|
||||
</span>
|
||||
<span className="menubar-clock">
|
||||
{time}<span className="cypher-caret">█</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTime(d: Date) {
|
||||
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const hh = String(d.getUTCHours()).padStart(2, '0');
|
||||
const mm = String(d.getUTCMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getUTCSeconds()).padStart(2, '0');
|
||||
return `${hh}:${mm}:${ss} UTC`;
|
||||
}
|
||||
|
||||
function formatUptime(ms: number) {
|
||||
const s = Math.floor(ms / 1000);
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
return `${String(h).padStart(2, '0')}h${String(m).padStart(2, '0')}m${String(sec).padStart(2, '0')}s`;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,18 @@ export const APPS: AppConfig[] = [
|
||||
component: Trash,
|
||||
desktopPosition: { col: 1, row: 3 },
|
||||
},
|
||||
{
|
||||
id: 'collab',
|
||||
title: 'Collab',
|
||||
emoji: '',
|
||||
iconGradient: 'linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02))',
|
||||
iconImage: '/docnest-icon-32.png',
|
||||
defaultSize: { width: 400, height: 360 },
|
||||
defaultPosition: { x: 0, y: 0 },
|
||||
component: Links,
|
||||
desktopPosition: { col: 0, row: 4 },
|
||||
externalUrl: 'https://collab.m1ngdaxie.com/',
|
||||
},
|
||||
];
|
||||
|
||||
export const WALLPAPERS = [
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface AppConfig {
|
||||
defaultPosition: { x: number; y: number };
|
||||
component: ComponentType;
|
||||
desktopPosition: { col: number; row: number };
|
||||
externalUrl?: string;
|
||||
iconImage?: string;
|
||||
}
|
||||
|
||||
export interface OSState {
|
||||
|
||||
Reference in New Issue
Block a user