feat: interactive terminal portfolio with dino game + resume PDF download

This commit is contained in:
2026-04-17 02:08:46 +08:00
commit a05e75baae
3 changed files with 938 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
COPY Omair_Saleh_Resume.pdf /usr/share/nginx/html/Omair_Saleh_Resume.pdf
EXPOSE 80
Binary file not shown.
+934
View File
@@ -0,0 +1,934 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>omair@quikcue:~$</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a0a;
--green: #00ff41;
--red: #ff6b6b;
--blue: #61dafb;
--gold: #ffd700;
--dim: #4a4a4a;
--mid: #888;
--white: #e0e0e0;
--font: 'JetBrains Mono', monospace;
--fs: clamp(12px, 2.2vw, 15px);
}
html, body {
height: 100%;
background: var(--bg);
color: var(--green);
font-family: var(--font);
font-size: var(--fs);
line-height: 1.6;
overflow: hidden;
}
#terminal {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 900px;
margin: 0 auto;
padding: 0 12px 12px;
}
/* ── Top bar ── */
#topbar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 0 8px;
border-bottom: 1px solid #1a1a1a;
flex-shrink: 0;
user-select: none;
}
.dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.dot-r { background: #ff5f57; }
.dot-y { background: #febc2e; }
.dot-g { background: #28c840; }
#topbar-title {
flex: 1;
text-align: center;
color: var(--mid);
font-size: 0.85em;
letter-spacing: 0.04em;
}
/* ── Output area ── */
#output {
flex: 1;
overflow-y: auto;
padding: 12px 0;
scroll-behavior: smooth;
}
#output::-webkit-scrollbar { width: 4px; }
#output::-webkit-scrollbar-track { background: transparent; }
#output::-webkit-scrollbar-thumb { background: #222; border-radius: 2px; }
.line {
white-space: pre-wrap;
word-break: break-word;
min-height: 1.6em;
}
.line.cmd { color: var(--green); }
.line.out { color: var(--white); }
.line.err { color: var(--red); }
.line.hi { color: var(--blue); }
.line.gold { color: var(--gold); }
.line.dim { color: var(--dim); }
.line.mid { color: var(--mid); }
.line.green { color: var(--green); }
.line.empty { height: 0.8em; }
/* ── Prompt row ── */
#input-row {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
padding-top: 4px;
}
#prompt-label {
color: var(--green);
white-space: nowrap;
flex-shrink: 0;
user-select: none;
}
#prompt-label span.at { color: var(--blue); }
#prompt-label span.host { color: var(--gold); }
#prompt-label span.sep { color: var(--mid); }
#prompt-label span.dir { color: var(--green); }
#prompt-label span.dol { color: var(--white); }
#cmd-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--green);
font-family: var(--font);
font-size: var(--fs);
caret-color: var(--green);
min-width: 0;
}
#cmd-input::selection { background: #00ff4133; }
/* Blinking cursor simulation (we keep native caret + add blink effect) */
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
.cursor-block {
display: inline-block;
width: 0.6em;
height: 1em;
background: var(--green);
vertical-align: text-bottom;
animation: blink 1.1s step-start infinite;
margin-left: 1px;
}
/* ── Dino game ── */
#dino-container {
display: none;
border: 1px solid #1e1e1e;
border-radius: 4px;
padding: 10px;
margin: 8px 0;
background: #050505;
position: relative;
user-select: none;
}
#dino-canvas {
display: block;
width: 100%;
height: 120px;
image-rendering: pixelated;
}
#dino-score-bar {
display: flex;
justify-content: space-between;
color: var(--mid);
font-size: 0.85em;
margin-bottom: 6px;
}
#dino-msg {
text-align: center;
color: var(--gold);
font-size: 0.85em;
min-height: 1.4em;
}
/* ── Boot animation ── */
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
.boot-line { animation: fadeIn 0.1s ease forwards; opacity: 0; }
/* ── Progress bar ── */
.progress-bar { color: var(--green); letter-spacing: -1px; }
/* ── Help table ── */
.help-cmd { color: var(--gold); }
.help-sep { color: var(--dim); }
.help-desc { color: var(--white); }
/* Scrollbar for Firefox */
#output { scrollbar-width: thin; scrollbar-color: #222 transparent; }
/* Mobile touch — larger tap target for input */
@media (max-width: 600px) {
#topbar-title { display: none; }
#terminal { padding: 0 8px 8px; }
}
</style>
</head>
<body>
<div id="terminal">
<div id="topbar">
<div class="dot dot-r"></div>
<div class="dot dot-y"></div>
<div class="dot dot-g"></div>
<div id="topbar-title">omair@quikcue — terminal portfolio</div>
</div>
<div id="output"></div>
<div id="input-row">
<div id="prompt-label">
<span class="at">omair</span><span class="sep">@</span><span class="host">quikcue</span><span class="sep">:</span><span class="dir">~</span><span class="dol">$</span>
</div>
<input id="cmd-input" type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" aria-label="terminal input">
</div>
</div>
<!-- Dino game (hidden until invoked) -->
<div id="dino-container" style="display:none; position:fixed; bottom:0; left:0; right:0; z-index:100; background:#050505; border-top:1px solid #1e1e1e; padding:10px 16px;">
<div id="dino-score-bar">
<span>DINO RUNNER</span>
<span>SCORE: <span id="dino-score-val">0</span> &nbsp; HI: <span id="dino-hi-val">0</span></span>
</div>
<canvas id="dino-canvas" width="800" height="120"></canvas>
<div id="dino-msg">SPACE / ↑ to jump &nbsp;|&nbsp; ESC to quit</div>
</div>
<script>
'use strict';
/* ─────────────────────────── helpers ─────────────────────────── */
const out = document.getElementById('output');
const inp = document.getElementById('cmd-input');
let dinoActive = false;
function scrollBottom() {
out.scrollTop = out.scrollHeight;
}
function addLine(text = '', cls = 'out') {
if (text === '') { const d = document.createElement('div'); d.className='line empty'; out.appendChild(d); scrollBottom(); return; }
// Support newlines inside text
text.split('\n').forEach(t => {
const d = document.createElement('div');
d.className = 'line ' + cls;
d.textContent = t;
out.appendChild(d);
});
scrollBottom();
}
function addHTML(html, cls = 'out') {
const d = document.createElement('div');
d.className = 'line ' + cls;
d.innerHTML = html;
out.appendChild(d);
scrollBottom();
}
function printPrompt(cmd) {
addHTML(
`<span style="color:var(--blue)">omair</span><span style="color:var(--mid)">@</span><span style="color:var(--gold)">quikcue</span><span style="color:var(--mid)">:</span><span style="color:var(--green)">~</span><span style="color:var(--white)">$</span> <span style="color:var(--green)">${escHtml(cmd)}</span>`,
'cmd'
);
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* ─────────────────────────── command history ─────────────────── */
const history = [];
let histIdx = -1;
/* ─────────────────────────── commands ───────────────────────── */
const COMMANDS = ['help','about','experience','skills','projects','stats','contact','git log','cat resume.pdf','download','sudo hire omair','dino','play dino','game','clear','exit'];
const ALIASES = { 'game': 'dino', 'play dino': 'dino' };
function runCommand(raw) {
const cmd = raw.trim();
if (!cmd) return;
// history
if (history[0] !== cmd) history.unshift(cmd);
if (history.length > 50) history.pop();
histIdx = -1;
if (dinoActive) { addLine('Game active. Press ESC to quit.', 'err'); return; }
printPrompt(cmd);
const key = ALIASES[cmd.toLowerCase()] || cmd.toLowerCase();
switch (key) {
case 'help': cmdHelp(); break;
case 'about': cmdAbout(); break;
case 'experience': cmdExperience(); break;
case 'skills': cmdSkills(); break;
case 'projects': cmdProjects(); break;
case 'stats': cmdStats(); break;
case 'contact': cmdContact(); break;
case 'git log': cmdGitLog(); break;
case 'cat resume.pdf': cmdCatResume(); break;
case 'download': cmdDownload(); break;
case 'sudo hire omair': cmdHire(); break;
case 'dino': cmdDino(); break;
case 'clear': cmdClear(); break;
case 'exit': cmdExit(); break;
default:
addLine(`Command not found: ${cmd}. Type 'help' for available commands.`, 'err');
}
}
/* ── help ─────────────────────────────────────────────────────── */
function cmdHelp() {
addLine();
addLine(' Available Commands', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
const rows = [
['about', 'Who is Omair Saleh'],
['experience', 'Career timeline (12 years)'],
['skills', 'Languages, frameworks, tools'],
['projects', 'Notable production projects'],
['stats', 'Git contribution stats'],
['contact', 'Email, LinkedIn, phone'],
['git log', 'Recent commit history'],
['cat resume.pdf', 'Easter egg 🐣'],
['download', 'Get the resume PDF'],
['sudo hire omair', 'The most important command'],
['dino', 'Play the dinosaur game 🦕'],
['clear', 'Clear the terminal'],
['exit', 'Say goodbye'],
];
rows.forEach(([c, d]) => {
addHTML(
` <span class="help-cmd">${c.padEnd(20)}</span><span class="help-sep">— </span><span class="help-desc">${d}</span>`
);
});
addLine();
addLine(' Tip: Use ↑ / ↓ for history, Tab to autocomplete.', 'dim');
addLine();
}
/* ── about ────────────────────────────────────────────────────── */
function cmdAbout() {
addLine();
addLine(' Omair Saleh', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine(' Senior Full-Stack Engineer with 12 years of experience.', 'out');
addLine(' Builds web apps, payment platforms, data pipelines,', 'out');
addLine(' and AI-enabled workflows.', 'out');
addLine();
addLine(' Shipped 29+ production applications across fintech,', 'out');
addLine(' logistics, nonprofit, and SaaS.', 'out');
addLine();
addLine(' Remote (UTC+8) · Malaysia', 'hi');
addLine();
}
/* ── experience ───────────────────────────────────────────────── */
function cmdExperience() {
addLine();
addLine(' Career Timeline', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine();
const roles = [
{
branch: '┌─',
title: 'Systems & Automation Consultant',
co: 'QuikCue',
period: 'Jul 2025 Present',
lines: [
'│ Built 8 production apps in 7 weeks. SaaS pledge platform,',
'│ AI assistant with 37 tools, clinic management, analytics dashboards.',
]
},
{
branch: '├─',
title: 'Lead Full-Stack Developer',
co: 'Charity Right',
period: 'Oct 2024 Present',
lines: [
'│ Sole tech hire for UK charity (~$4M/year). Built donation data',
'│ warehouse, Next.js checkout, 25+ analytics reports, fraud prevention.',
]
},
{
branch: '├─',
title: 'Technical Operations Lead',
co: 'C.I.E.F Worldwide',
period: 'Sep 2018 Sep 2024',
lines: [
'│ 6 years, 4,300+ commits. Currency exchange platform, shipping portal,',
'│ 1688/Alibaba integration, billing engine.',
]
},
{
branch: '├─',
title: 'Full-Stack Developer',
co: 'IP ServerOne',
period: 'Sep 2017 Sep 2018',
lines: []
},
{
branch: '├─',
title: 'Independent Web Developer',
co: 'MediaSkip Enterprise',
period: 'Sep 2016 Sep 2017',
lines: []
},
{
branch: '└─',
title: 'Web Developer',
co: 'Monkey Media Web Agency',
period: 'Dec 2014 Dec 2016',
lines: []
},
];
roles.forEach((r, i) => {
addHTML(
` <span style="color:var(--dim)">${r.branch}</span> <span style="color:var(--gold)">${r.title}</span>` +
` <span style="color:var(--dim)">──</span> <span style="color:var(--blue)">${r.co}</span>` +
` <span style="color:var(--mid)">${r.period}</span>`
);
r.lines.forEach(l => addLine(' ' + l, 'out'));
if (i < roles.length - 1 && r.lines.length > 0) addLine(' │', 'dim');
else if (i < roles.length - 1 && r.lines.length === 0) { /* no gap for short entries */ }
});
addLine();
}
/* ── skills ───────────────────────────────────────────────────── */
function cmdSkills() {
addLine();
addLine(' Technical Skills', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine();
const groups = [
{ label: 'Languages ', items: 'TypeScript · JavaScript · PHP · Python · SQL · Shell', color: 'var(--green)' },
{ label: 'Frameworks ', items: 'Laravel · Next.js · React · Vue.js · Filament · Prisma', color: 'var(--blue)' },
{ label: 'Payments ', items: 'Stripe · GoCardless · PayPal · Billplz · Apple Pay', color: 'var(--gold)' },
{ label: 'AI ', items: 'GPT-5 · Gemini · Agent Orchestration · Generative UI', color: 'var(--blue)' },
{ label: 'Infra ', items: 'Docker · Nginx · Traefik · PostgreSQL · MySQL · Redis', color: 'var(--mid)' },
{ label: 'Integrations', items: 'WhatsApp · Auth0 · 1688/Alibaba · Meta Ads · N3O CRM', color: 'var(--green)' },
];
groups.forEach(g => {
addHTML(
` <span style="color:var(--dim)">${g.label}</span> <span style="color:${g.color}">${g.items}</span>`
);
});
addLine();
}
/* ── projects ─────────────────────────────────────────────────── */
function cmdProjects() {
addLine();
addLine(' Notable Projects', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine();
const projects = [
{ id: '1', name: 'PledgeNow ', desc: 'SaaS pledge-to-payment platform (Stripe Connect, GoCardless, WhatsApp)' },
{ id: '2', name: 'Charity Right ', desc: 'Donation platform, data warehouse, 25+ analytics reports' },
{ id: '3', name: 'Exchange 2.0 ', desc: 'Cross-border currency exchange (MYR/RMB/USD) with wallet system' },
{ id: '4', name: 'Shipping Portal', desc: 'End-to-end logistics (China→Malaysia) with 3 carrier APIs' },
{ id: '5', name: 'Calvana ', desc: 'Charity ops dashboard — 11 AI features shipped in 3 days' },
{ id: '6', name: 'Clinera ', desc: 'WhatsApp-first clinic management for GCC region' },
];
projects.forEach(p => {
addHTML(
` <span style="color:var(--dim)">[</span><span style="color:var(--gold)">${p.id}</span><span style="color:var(--dim)">]</span>` +
` <span style="color:var(--blue)">${p.name}</span>` +
` <span style="color:var(--white)">${p.desc}</span>`
);
});
addLine();
}
/* ── stats ────────────────────────────────────────────────────── */
function cmdStats() {
addLine();
addLine(' Git Contribution Stats', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine();
const stats = [
['Total Commits ', '4,700+'],
['Repos Contributed', '29+'],
['Years Active ', '12'],
['Largest Platform ', 'Exchange 2.0 (1,798 commits over 4 years)'],
['Fastest Ship ', 'JustVitamin (4 versions in 1 day)'],
['Most Intense ', '8 production apps in 7 weeks (QuikCue)'],
];
stats.forEach(([k, v]) => {
addHTML(
` <span style="color:var(--dim)">${k}</span> <span style="color:var(--green)">${v}</span>`
);
});
addLine();
}
/* ── contact ──────────────────────────────────────────────────── */
function cmdContact() {
addLine();
addLine(' Contact', 'gold');
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine();
addHTML(` <span style="color:var(--dim)">Email </span> <a href="mailto:omair@quikcue.com" style="color:var(--blue);text-decoration:none;" target="_blank">omair@quikcue.com</a>`);
addHTML(` <span style="color:var(--dim)">LinkedIn </span> <a href="https://linkedin.com/in/omair-rescues" style="color:var(--blue);text-decoration:none;" target="_blank">linkedin.com/in/omair-rescues</a>`);
addHTML(` <span style="color:var(--dim)">Phone </span> <span style="color:var(--white)">+601162222512</span>`);
addHTML(` <span style="color:var(--dim)">Timezone </span> <span style="color:var(--mid)">UTC+8 (Malaysia)</span>`);
addLine();
}
/* ── git log ──────────────────────────────────────────────────── */
function cmdGitLog() {
addLine();
addHTML(` <span style="color:var(--gold)">commit log</span> <span style="color:var(--dim)">(HEAD → main, origin/main)</span>`);
addLine(' ──────────────────────────────────────────────────────', 'dim');
addLine();
const logs = [
{ hash: 'a3f7b2e', date: '2026-04-14', msg: 'feat: AI self-improvement feedback loop with DB persistence' },
{ hash: '8c1d4f9', date: '2026-04-11', msg: 'fix: auth guards on 20+ unprotected API routes' },
{ hash: '2e9a1b3', date: '2026-04-09', msg: 'feat: generative UI canvas with 12-col adaptive layout' },
{ hash: 'f1c3a07', date: '2026-04-06', msg: 'feat: WhatsApp webhook — appointment reminders & confirmations' },
{ hash: 'd8b24e1', date: '2026-04-02', msg: 'refactor: payment intent flow extracted to reusable service layer' },
{ hash: '7a9f3c5', date: '2026-03-28', msg: 'feat: donor data warehouse — 25 analytics reports, 0 raw queries' },
{ hash: 'c4e2b81', date: '2026-03-24', msg: 'feat: Stripe Connect onboarding + payout scheduling' },
{ hash: '91d6a3f', date: '2026-03-20', msg: 'fix: race condition in concurrent pledge fulfillment queue' },
{ hash: 'b52c7e9', date: '2026-03-16', msg: 'chore: migrate 4-year Laravel monolith to TypeScript + Prisma' },
{ hash: '3f8d1a4', date: '2026-03-10', msg: 'feat: GoCardless DD mandate flow with failure retry engine' },
];
logs.forEach(l => {
addHTML(
` <span style="color:var(--gold)">${l.hash}</span>` +
` <span style="color:var(--mid)">${l.date}</span>` +
` <span style="color:var(--white)">${l.msg}</span>`
);
});
addLine();
}
/* ── cat resume.pdf ───────────────────────────────────────────── */
function cmdCatResume() {
addLine();
addLine(" Nice try. The PDF is what brought you here. 😉", 'hi');
addLine(" Type 'download' to get the actual PDF.", 'mid');
addLine();
}
/* ── download ─────────────────────────────────────────────────── */
function cmdDownload() {
addLine();
addLine(' 📄 Downloading resume...', 'hi');
const a = document.createElement('a');
a.href = './Omair_Saleh_Resume.pdf';
a.download = 'Omair_Saleh_Resume.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => {
addLine(' ✅ Download started!', 'green');
addHTML(` Or open directly: <a href="./Omair_Saleh_Resume.pdf" style="color:var(--blue);text-decoration:underline;" target="_blank">Omair_Saleh_Resume.pdf</a>`);
addLine();
}, 500);
}
/* ── sudo hire omair ──────────────────────────────────────────── */
function cmdHire() {
addLine();
const steps = [
{ text: ' [sudo] password for recruiter: ********', cls: 'mid', delay: 0 },
{ text: ' Verifying credentials...', cls: 'out', delay: 700 },
{ text: ' Verifying credentials... ✓', cls: 'green', delay: 1400, replace: true },
{ text: ' Checking availability...', cls: 'out', delay: 1600 },
{ text: ' Checking availability... ✓', cls: 'green', delay: 2300, replace: true },
{ text: ' Matching skills to requirements...', cls: 'out', delay: 2500 },
{ text: ' Matching skills to requirements... ✓', cls: 'green', delay: 3400, replace: true },
{ text: '', cls: 'empty', delay: 3600 },
{ text: ' ✅ Hiring approved. Omair has been added to your team.', cls: 'gold', delay: 3700 },
{ text: ' Starting date: Whenever you\'re ready.', cls: 'hi', delay: 4000 },
{ text: '', cls: 'empty', delay: 4200 },
{ text: " Contact: omair@quikcue.com | +601162222512", cls: 'green', delay: 4400 },
{ text: '', cls: 'empty', delay: 4500 },
];
let lastEl = null;
steps.forEach(s => {
setTimeout(() => {
if (s.replace && lastEl) {
lastEl.textContent = s.text;
lastEl.className = 'line ' + s.cls;
} else {
if (s.cls === 'empty') {
const d = document.createElement('div');
d.className = 'line empty';
out.appendChild(d);
} else {
const d = document.createElement('div');
d.className = 'line ' + s.cls;
d.textContent = s.text;
out.appendChild(d);
lastEl = d;
}
}
scrollBottom();
}, s.delay);
});
}
/* ── clear ────────────────────────────────────────────────────── */
function cmdClear() {
out.innerHTML = '';
}
/* ── exit ─────────────────────────────────────────────────────── */
function cmdExit() {
addLine();
addLine(" Thanks for visiting! Don't forget to reach out:", 'out');
addHTML(` <a href="mailto:omair@quikcue.com" style="color:var(--blue)">omair@quikcue.com</a>`);
addLine();
addLine(' Closing session...', 'dim');
setTimeout(() => {
addLine(' Connection closed.', 'dim');
inp.disabled = true;
}, 1200);
}
/* ─────────────────────────── Dino Game ──────────────────────── */
function cmdDino() {
addLine();
addLine(' Launching Dino Runner… Press SPACE or ↑ to jump!', 'gold');
addLine(' (Press ESC to quit)', 'dim');
addLine();
startDinoGame();
}
(function DinoGame() {
const container = document.getElementById('dino-container');
const canvas = document.getElementById('dino-canvas');
const scoreEl = document.getElementById('dino-score-val');
const hiEl = document.getElementById('dino-hi-val');
const msgEl = document.getElementById('dino-msg');
const ctx = canvas.getContext('2d');
const W = 800, H = 120;
const GROUND = H - 20;
const DINO_W = 28, DINO_H = 34;
const CACTUS_W = 16;
const GRAVITY = 0.7;
const JUMP_V = -13;
let state, dino, cacti, score, hiScore, speed, frameId, spawnTimer;
hiScore = 0;
function reset() {
state = 'idle'; // idle | running | dead
dino = { x: 60, y: GROUND - DINO_H, vy: 0, onGround: true };
cacti = [];
score = 0;
speed = 4;
spawnTimer = 80;
msgEl.textContent = 'SPACE / ↑ to jump | ESC to quit';
}
function jump() {
if (state === 'dead') { reset(); state = 'running'; return; }
if (state === 'idle') { state = 'running'; return; }
if (dino.onGround) {
dino.vy = JUMP_V;
dino.onGround = false;
}
}
function drawGround() {
ctx.strokeStyle = '#1e5c1e';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, GROUND + DINO_H);
ctx.lineTo(W, GROUND + DINO_H);
ctx.stroke();
}
function drawDino(x, y) {
ctx.fillStyle = '#00ff41';
// Body
ctx.fillRect(x + 4, y + 10, 18, 20);
// Head
ctx.fillRect(x + 10, y, 14, 14);
// Eye
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(x + 19, y + 3, 3, 3);
// Tail
ctx.fillStyle = '#00ff41';
ctx.fillRect(x, y + 14, 8, 8);
// Legs (alternate based on score)
const leg = Math.floor(score / 6) % 2;
ctx.fillRect(x + 6, y + 30, 5, leg === 0 ? 8 : 4);
ctx.fillRect(x + 14, y + 30, 5, leg === 1 ? 8 : 4);
}
function drawCactus(x) {
ctx.fillStyle = '#ff6b6b';
const h = 30 + (Math.floor(x) % 2) * 10;
ctx.fillRect(x, GROUND + DINO_H - h, CACTUS_W, h);
ctx.fillRect(x - 6, GROUND + DINO_H - h * 0.6, 6, 8);
ctx.fillRect(x + CACTUS_W, GROUND + DINO_H - h * 0.5, 6, 8);
}
function collides(c) {
const pad = 4;
return (
dino.x + pad + 10 < c.x + CACTUS_W - pad &&
dino.x + DINO_W - pad > c.x + pad &&
dino.y + pad < GROUND + DINO_H &&
dino.y + DINO_H > GROUND + DINO_H - 40
);
}
function frame() {
ctx.clearRect(0, 0, W, H);
drawGround();
if (state === 'idle') {
drawDino(dino.x, dino.y);
ctx.fillStyle = '#4a4a4a';
ctx.font = '14px JetBrains Mono, monospace';
ctx.fillText('PRESS SPACE TO START', W/2 - 110, H/2 - 10);
frameId = requestAnimationFrame(frame);
return;
}
if (state === 'dead') {
drawDino(dino.x, dino.y);
cacti.forEach(c => drawCactus(c.x));
ctx.fillStyle = '#ff6b6b';
ctx.font = 'bold 16px JetBrains Mono, monospace';
ctx.fillText('GAME OVER!', W/2 - 52, H/2 - 8);
frameId = requestAnimationFrame(frame);
return;
}
// Physics
dino.vy += GRAVITY;
dino.y += dino.vy;
if (dino.y >= GROUND - DINO_H) {
dino.y = GROUND - DINO_H;
dino.vy = 0;
dino.onGround = true;
}
// Spawn cacti
spawnTimer--;
if (spawnTimer <= 0) {
cacti.push({ x: W + 10 });
spawnTimer = 60 + Math.floor(Math.random() * 60);
}
// Move cacti
cacti.forEach(c => c.x -= speed);
cacti = cacti.filter(c => c.x > -40);
// Score
score++;
speed = 4 + Math.floor(score / 300) * 0.5;
scoreEl.textContent = Math.floor(score / 6);
// Collision
if (cacti.some(c => collides(c))) {
state = 'dead';
const s = Math.floor(score / 6);
if (s > hiScore) { hiScore = s; hiEl.textContent = hiScore; }
msgEl.textContent = `GAME OVER! Score: ${s} | SPACE to restart | ESC to quit`;
}
drawDino(dino.x, dino.y);
cacti.forEach(c => drawCactus(c.x));
frameId = requestAnimationFrame(frame);
}
window.startDinoGame = function() {
if (dinoActive) return;
dinoActive = true;
container.style.display = 'block';
inp.blur();
reset();
frameId = requestAnimationFrame(frame);
};
window.stopDinoGame = function() {
dinoActive = false;
container.style.display = 'none';
if (frameId) cancelAnimationFrame(frameId);
inp.focus();
addLine(' Dino game closed. Score: ' + Math.floor(score / 6), 'mid');
addLine();
};
// Controls
document.addEventListener('keydown', e => {
if (!dinoActive) return;
if (e.code === 'Space' || e.code === 'ArrowUp') {
e.preventDefault();
jump();
}
if (e.code === 'Escape') {
stopDinoGame();
}
});
// Touch for mobile
canvas.addEventListener('touchstart', e => {
e.preventDefault();
jump();
}, { passive: false });
reset();
})();
/* ─────────────────────────── Input handling ─────────────────── */
inp.addEventListener('keydown', e => {
if (dinoActive) return;
if (e.key === 'Enter') {
const val = inp.value;
inp.value = '';
runCommand(val);
return;
}
// History navigation
if (e.key === 'ArrowUp') {
e.preventDefault();
if (histIdx < history.length - 1) {
histIdx++;
inp.value = history[histIdx];
// cursor to end
setTimeout(() => inp.setSelectionRange(inp.value.length, inp.value.length), 0);
}
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (histIdx > 0) {
histIdx--;
inp.value = history[histIdx];
} else {
histIdx = -1;
inp.value = '';
}
return;
}
// Tab autocomplete
if (e.key === 'Tab') {
e.preventDefault();
const partial = inp.value.toLowerCase();
if (!partial) return;
const match = COMMANDS.find(c => c.startsWith(partial));
if (match) inp.value = match;
return;
}
});
// Keep focus on input
document.addEventListener('click', () => {
if (!dinoActive) inp.focus();
});
document.addEventListener('keydown', e => {
if (!dinoActive && document.activeElement !== inp && e.key.length === 1) {
inp.focus();
}
});
/* ─────────────────────────── Boot sequence ─────────────────── */
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function boot() {
const bootLines = [
{ text: 'Booting omair-os v12.0...', cls: 'green', delay: 0 },
{ text: '', cls: 'empty', delay: 300 },
];
for (const l of bootLines) {
await sleep(l.delay);
if (l.cls === 'empty') {
const d = document.createElement('div'); d.className = 'line empty'; out.appendChild(d);
} else {
addLine(' ' + l.text, l.cls);
}
}
// Progress bar animation
await sleep(200);
const barEl = document.createElement('div');
barEl.className = 'line out';
barEl.innerHTML = ' Loading modules: [<span id="pbar"></span>] <span id="ppct">0%</span>';
out.appendChild(barEl);
scrollBottom();
const pbar = document.getElementById('pbar');
const ppct = document.getElementById('ppct');
const total = 20;
for (let i = 0; i <= total; i++) {
pbar.textContent = '█'.repeat(i) + '░'.repeat(total - i);
ppct.textContent = Math.round((i / total) * 100) + '%';
pbar.style.color = 'var(--green)';
scrollBottom();
await sleep(40);
}
ppct.style.color = 'var(--gold)';
await sleep(200);
addLine(' System ready.', 'green');
await sleep(300);
addLine();
addLine(' Welcome to Omair\'s Terminal Portfolio.', 'hi');
addLine(' Type \'help\' to see available commands.', 'mid');
addLine();
inp.focus();
// Auto-show help after short pause
await sleep(600);
runCommand('help');
}
boot();
</script>
</body>
</html>