Files
resume/index.html
T

935 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>