Initial WebOS portfolio site

This commit is contained in:
Greg Hendrickson
2026-01-27 22:26:18 +00:00
commit 9c61ec863b
6 changed files with 887 additions and 0 deletions

299
js/app.js Normal file
View File

@@ -0,0 +1,299 @@
// GregOS - WebOS Resume Site
class GregOS {
constructor() {
this.windows = new Map();
this.zIndex = 100;
this.windowOffset = { x: 50, y: 50 };
this.init();
}
init() {
this.bindIconClicks();
this.startClock();
this.setupTerminal();
}
bindIconClicks() {
document.querySelectorAll('.icon').forEach(icon => {
icon.addEventListener('dblclick', () => {
const windowId = icon.dataset.window;
this.openWindow(windowId);
});
// Mobile: single tap
icon.addEventListener('click', (e) => {
if (window.innerWidth <= 768) {
const windowId = icon.dataset.window;
this.openWindow(windowId);
}
});
});
}
openWindow(id) {
if (this.windows.has(id)) {
const win = this.windows.get(id);
win.element.classList.remove('minimized');
this.focusWindow(win.element);
return;
}
const template = document.getElementById('window-template');
const content = document.getElementById(`content-${id}`);
if (!template || !content) return;
const windowEl = template.content.cloneNode(true).querySelector('.window');
const title = document.querySelector(`[data-window="${id}"] span`).textContent;
windowEl.querySelector('.window-title').textContent = title;
windowEl.querySelector('.window-content').innerHTML = content.innerHTML;
// If terminal, add terminal class
if (id === 'terminal') {
windowEl.querySelector('.window-content').classList.add('terminal-content');
}
// Position window
windowEl.style.left = this.windowOffset.x + 'px';
windowEl.style.top = this.windowOffset.y + 'px';
windowEl.style.width = '500px';
windowEl.style.height = '400px';
this.windowOffset.x += 30;
this.windowOffset.y += 30;
if (this.windowOffset.x > 300) this.windowOffset = { x: 50, y: 50 };
document.querySelector('.windows-container').appendChild(windowEl);
this.windows.set(id, { element: windowEl, title });
this.focusWindow(windowEl);
this.addTaskbarItem(id, title);
this.setupWindowControls(windowEl, id);
this.setupDrag(windowEl);
this.setupResize(windowEl);
// Focus terminal input
if (id === 'terminal') {
setTimeout(() => {
const input = windowEl.querySelector('#terminal-input');
if (input) input.focus();
}, 100);
}
}
setupWindowControls(windowEl, id) {
windowEl.querySelector('.close').addEventListener('click', () => {
windowEl.remove();
this.windows.delete(id);
this.removeTaskbarItem(id);
});
windowEl.querySelector('.minimize').addEventListener('click', () => {
windowEl.classList.add('minimized');
});
windowEl.querySelector('.maximize').addEventListener('click', () => {
if (windowEl.dataset.maximized === 'true') {
windowEl.style.left = windowEl.dataset.prevLeft;
windowEl.style.top = windowEl.dataset.prevTop;
windowEl.style.width = windowEl.dataset.prevWidth;
windowEl.style.height = windowEl.dataset.prevHeight;
windowEl.dataset.maximized = 'false';
} else {
windowEl.dataset.prevLeft = windowEl.style.left;
windowEl.dataset.prevTop = windowEl.style.top;
windowEl.dataset.prevWidth = windowEl.style.width;
windowEl.dataset.prevHeight = windowEl.style.height;
windowEl.style.left = '0';
windowEl.style.top = '0';
windowEl.style.width = '100%';
windowEl.style.height = 'calc(100vh - 50px)';
windowEl.dataset.maximized = 'true';
}
});
windowEl.addEventListener('mousedown', () => this.focusWindow(windowEl));
}
focusWindow(windowEl) {
this.zIndex++;
windowEl.style.zIndex = this.zIndex;
}
setupDrag(windowEl) {
const header = windowEl.querySelector('.window-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.window-controls')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(windowEl.style.left) || 0;
startTop = parseInt(windowEl.style.top) || 0;
windowEl.style.transition = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
windowEl.style.left = startLeft + dx + 'px';
windowEl.style.top = startTop + dy + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
windowEl.style.transition = '';
});
}
setupResize(windowEl) {
let isResizing = false;
let startX, startY, startWidth, startHeight;
windowEl.addEventListener('mousedown', (e) => {
const rect = windowEl.getBoundingClientRect();
if (e.clientX > rect.right - 15 && e.clientY > rect.bottom - 15) {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = rect.width;
startHeight = rect.height;
e.preventDefault();
}
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
windowEl.style.width = Math.max(400, startWidth + dx) + 'px';
windowEl.style.height = Math.max(300, startHeight + dy) + 'px';
});
document.addEventListener('mouseup', () => {
isResizing = false;
});
}
addTaskbarItem(id, title) {
const item = document.createElement('div');
item.className = 'taskbar-item';
item.dataset.window = id;
item.textContent = title;
item.addEventListener('click', () => {
const win = this.windows.get(id);
if (win) {
win.element.classList.toggle('minimized');
if (!win.element.classList.contains('minimized')) {
this.focusWindow(win.element);
}
}
});
document.querySelector('.taskbar-items').appendChild(item);
}
removeTaskbarItem(id) {
const item = document.querySelector(`.taskbar-item[data-window="${id}"]`);
if (item) item.remove();
}
startClock() {
const updateClock = () => {
const now = new Date();
const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
document.querySelector('.clock').textContent = time;
};
updateClock();
setInterval(updateClock, 1000);
}
setupTerminal() {
document.addEventListener('keydown', (e) => {
const input = document.getElementById('terminal-input');
if (!input || document.activeElement !== input) return;
if (e.key === 'Enter') {
this.executeCommand(input.value);
input.value = '';
}
});
}
executeCommand(cmd) {
const output = document.getElementById('terminal-output');
if (!output) return;
const addLine = (text, color = '#0f0') => {
const p = document.createElement('p');
p.textContent = text;
p.style.color = color;
output.appendChild(p);
output.scrollTop = output.scrollHeight;
};
addLine(`guest@gregos:~$ ${cmd}`);
const commands = {
help: () => {
addLine('Available commands:');
addLine(' help - Show this help');
addLine(' about - About me');
addLine(' projects - List projects');
addLine(' skills - Show skills');
addLine(' contact - Contact info');
addLine(' clear - Clear terminal');
addLine(' neofetch - System info');
},
about: () => {
addLine('Greg Hendrickson');
addLine('Developer, tinkerer, builder of things.');
},
projects: () => {
addLine('Projects:');
addLine(' - ShellMate (shellmate.sh)');
addLine(' - Clawdbot');
},
skills: () => {
addLine('Skills: Python, TypeScript, Go, Linux, Docker, AI/ML');
},
contact: () => {
addLine('GitHub: github.com/ghndrx');
},
clear: () => {
output.innerHTML = '';
},
neofetch: () => {
addLine(' ____ ___ ____ ', '#e94560');
addLine(' / ___|_ __ ___ __ _ / _ \\/ ___| ', '#e94560');
addLine(' | | _| \'__/ _ \\/ _` | | | \\___ \\ ', '#e94560');
addLine(' | |_| | | | __/ (_| | |_| |___) |', '#e94560');
addLine(' \\____|_| \\___|\\__, |\\___/|____/ ', '#e94560');
addLine(' |___/ ', '#e94560');
addLine('');
addLine('OS: GregOS v1.0');
addLine('Host: The Internet');
addLine('Uptime: Since the early days');
addLine('Shell: bash');
addLine('Terminal: WebTerminal');
}
};
const trimmedCmd = cmd.trim().toLowerCase();
if (trimmedCmd === '') return;
if (commands[trimmedCmd]) {
commands[trimmedCmd]();
} else {
addLine(`Command not found: ${cmd}. Type 'help' for available commands.`, '#ff6b6b');
}
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
window.gregos = new GregOS();
});