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

36
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: "index.html,css/,js/,assets/"
target: "/var/www/webos"
strip_components: 0
- name: Set permissions
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
script: |
sudo chown -R www-data:www-data /var/www/webos
sudo chmod -R 755 /var/www/webos

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_Store
*.log
node_modules/
.env

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# WebOS - Greg's Portfolio
A WebOS-style personal resume and portfolio site.
## Features
- 🖥️ Desktop-like interface with draggable, resizable windows
- 📁 Multiple "apps": About, Projects, Resume, Contact, Terminal
- 🎮 Interactive terminal with commands
- 📱 Mobile responsive
## Local Development
Just open `index.html` in a browser, or serve with:
```bash
python3 -m http.server 8000
# or
npx serve .
```
## Deployment
Automatically deploys to production on push to `main` via GitHub Actions.
**Live site**: https://ghndrx.dev (or your domain)
## License
MIT

382
css/style.css Normal file
View File

@@ -0,0 +1,382 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-window: #0f0f23;
--accent: #e94560;
--accent-secondary: #0f3460;
--text-primary: #eee;
--text-secondary: #aaa;
--border: #333;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
}
.desktop {
height: 100vh;
display: flex;
flex-direction: column;
background-image:
radial-gradient(circle at 20% 80%, rgba(233, 69, 96, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(15, 52, 96, 0.2) 0%, transparent 50%);
}
/* Desktop Icons */
.desktop-icons {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 10px;
align-content: flex-start;
max-height: calc(100vh - 50px);
}
.icon {
width: 80px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease;
}
.icon:hover {
background: rgba(255, 255, 255, 0.1);
}
.icon i {
font-size: 32px;
color: var(--accent);
}
.icon span {
font-size: 11px;
text-align: center;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
/* Windows */
.windows-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 50px;
pointer-events: none;
}
.window {
position: absolute;
min-width: 400px;
min-height: 300px;
background: var(--bg-window);
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
overflow: hidden;
pointer-events: auto;
display: flex;
flex-direction: column;
border: 1px solid var(--border);
}
.window.minimized {
display: none;
}
.window-header {
background: var(--bg-secondary);
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
border-bottom: 1px solid var(--border);
}
.window-title {
font-weight: 500;
font-size: 14px;
}
.window-controls {
display: flex;
gap: 8px;
}
.window-controls button {
width: 24px;
height: 24px;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
color: var(--text-secondary);
transition: all 0.2s ease;
}
.window-controls button:hover {
background: rgba(255, 255, 255, 0.2);
color: var(--text-primary);
}
.window-controls .close:hover {
background: var(--accent);
color: white;
}
.window-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.window-content h1 {
margin-bottom: 15px;
color: var(--accent);
}
.window-content h2 {
margin: 20px 0 10px;
color: var(--text-primary);
font-size: 18px;
}
.window-content h3 {
margin: 15px 0 8px;
color: var(--text-secondary);
}
.window-content p {
margin-bottom: 10px;
line-height: 1.6;
color: var(--text-secondary);
}
.window-content ul {
margin-left: 20px;
margin-bottom: 10px;
}
.window-content li {
margin: 5px 0;
color: var(--text-secondary);
}
.window-content a {
color: var(--accent);
text-decoration: none;
}
.window-content a:hover {
text-decoration: underline;
}
/* Project Cards */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.project-card {
background: rgba(255, 255, 255, 0.05);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--border);
}
.project-card h3 {
margin: 0 0 10px;
color: var(--text-primary);
}
/* Skills */
.skills {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.skill-tag {
background: var(--accent-secondary);
padding: 5px 12px;
border-radius: 15px;
font-size: 12px;
color: var(--text-primary);
}
/* Contact */
.contact-list {
list-style: none;
margin: 0;
}
.contact-list li {
margin: 10px 0;
display: flex;
align-items: center;
gap: 10px;
}
.contact-list i {
width: 20px;
color: var(--accent);
}
/* Terminal */
.terminal-content {
background: #000;
font-family: 'Courier New', monospace;
padding: 15px;
display: flex;
flex-direction: column;
height: 100%;
}
.terminal-output {
flex: 1;
overflow-y: auto;
margin-bottom: 10px;
}
.terminal-output p {
color: #0f0;
margin: 2px 0;
font-size: 13px;
}
.terminal-input-line {
display: flex;
align-items: center;
gap: 8px;
}
.prompt {
color: #0f0;
font-size: 13px;
}
#terminal-input {
flex: 1;
background: transparent;
border: none;
color: #0f0;
font-family: inherit;
font-size: 13px;
outline: none;
}
/* Taskbar */
.taskbar {
height: 50px;
background: rgba(15, 15, 35, 0.95);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
padding: 0 10px;
border-top: 1px solid var(--border);
}
.start-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.start-button:hover {
background: rgba(255, 255, 255, 0.1);
}
.start-button i {
color: var(--accent);
}
.taskbar-items {
flex: 1;
display: flex;
gap: 5px;
margin-left: 10px;
}
.taskbar-item {
padding: 8px 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
cursor: pointer;
font-size: 13px;
border-left: 2px solid var(--accent);
}
.taskbar-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.taskbar-item.active {
background: rgba(255, 255, 255, 0.15);
}
.system-tray {
display: flex;
align-items: center;
gap: 15px;
}
.clock {
font-size: 13px;
color: var(--text-secondary);
}
/* Content templates (hidden) */
.content-template {
display: none;
}
/* Resize handle */
.window::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
width: 15px;
height: 15px;
cursor: nwse-resize;
}
/* Responsive */
@media (max-width: 768px) {
.desktop-icons {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.window {
min-width: 90vw;
min-height: 60vh;
}
}

136
index.html Normal file
View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Greg Hendrickson | Developer</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
</head>
<body>
<div class="desktop">
<!-- Desktop Icons -->
<div class="desktop-icons">
<div class="icon" data-window="about">
<i class="fas fa-user"></i>
<span>About Me</span>
</div>
<div class="icon" data-window="projects">
<i class="fas fa-folder"></i>
<span>Projects</span>
</div>
<div class="icon" data-window="resume">
<i class="fas fa-file-alt"></i>
<span>Resume</span>
</div>
<div class="icon" data-window="contact">
<i class="fas fa-envelope"></i>
<span>Contact</span>
</div>
<div class="icon" data-window="terminal">
<i class="fas fa-terminal"></i>
<span>Terminal</span>
</div>
</div>
<!-- Windows Container -->
<div class="windows-container"></div>
<!-- Taskbar -->
<div class="taskbar">
<div class="start-button">
<i class="fas fa-circle"></i>
<span>Start</span>
</div>
<div class="taskbar-items"></div>
<div class="system-tray">
<span class="clock"></span>
</div>
</div>
</div>
<!-- Window Templates -->
<template id="window-template">
<div class="window">
<div class="window-header">
<span class="window-title"></span>
<div class="window-controls">
<button class="minimize"><i class="fas fa-minus"></i></button>
<button class="maximize"><i class="fas fa-square"></i></button>
<button class="close"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="window-content"></div>
</div>
</template>
<!-- Content Templates -->
<div id="content-about" class="content-template">
<h1>👋 Hey, I'm Greg</h1>
<p>Developer, tinkerer, and builder of things that probably shouldn't exist but do anyway.</p>
<p>I enjoy making AI slop into cool websites and building tools that make life easier (or at least more interesting).</p>
<h3>Current Focus</h3>
<ul>
<li>🤖 AI-powered automation</li>
<li>🐚 ShellMate - SSH-based games</li>
<li>🔧 Developer tooling</li>
</ul>
</div>
<div id="content-projects" class="content-template">
<h1>📁 Projects</h1>
<div class="project-grid">
<div class="project-card">
<h3>🐚 ShellMate</h3>
<p>SSH-based gaming platform. Play chess, puzzles, and more right in your terminal.</p>
<a href="https://shellmate.sh" target="_blank">shellmate.sh</a>
</div>
<div class="project-card">
<h3>🤖 Clawdbot</h3>
<p>AI assistant framework for automating everything.</p>
<a href="https://github.com/clawdbot/clawdbot" target="_blank">GitHub</a>
</div>
</div>
</div>
<div id="content-resume" class="content-template">
<h1>📄 Resume</h1>
<h2>Experience</h2>
<div class="resume-item">
<h3>Developer & Tinkerer</h3>
<p>Building cool shit since forever</p>
</div>
<h2>Skills</h2>
<div class="skills">
<span class="skill-tag">Python</span>
<span class="skill-tag">TypeScript</span>
<span class="skill-tag">Go</span>
<span class="skill-tag">Linux</span>
<span class="skill-tag">Docker</span>
<span class="skill-tag">AI/ML</span>
</div>
</div>
<div id="content-contact" class="content-template">
<h1>📬 Contact</h1>
<p>Want to chat? Here's how to reach me:</p>
<ul class="contact-list">
<li><i class="fab fa-github"></i> <a href="https://github.com/ghndrx" target="_blank">GitHub</a></li>
<li><i class="fas fa-envelope"></i> <a href="mailto:greg@example.com">Email</a></li>
</ul>
</div>
<div id="content-terminal" class="content-template terminal-content">
<div class="terminal-output" id="terminal-output">
<p>Welcome to GregOS v1.0</p>
<p>Type 'help' for available commands.</p>
</div>
<div class="terminal-input-line">
<span class="prompt">guest@gregos:~$</span>
<input type="text" id="terminal-input" autofocus>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>

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();
});