From 9c61ec863b8b1ffe86cd3afa45e0fe8fdb7da6b2 Mon Sep 17 00:00:00 2001 From: Greg Hendrickson Date: Tue, 27 Jan 2026 22:26:18 +0000 Subject: [PATCH] Initial WebOS portfolio site --- .github/workflows/deploy.yml | 36 ++++ .gitignore | 4 + README.md | 30 +++ css/style.css | 382 +++++++++++++++++++++++++++++++++++ index.html | 136 +++++++++++++ js/app.js | 299 +++++++++++++++++++++++++++ 6 files changed, 887 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 css/style.css create mode 100644 index.html create mode 100644 js/app.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..42a6bf4 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18b2cef --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +*.log +node_modules/ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2294a6 --- /dev/null +++ b/README.md @@ -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 diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..35ccf03 --- /dev/null +++ b/css/style.css @@ -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; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..db04843 --- /dev/null +++ b/index.html @@ -0,0 +1,136 @@ + + + + + + Greg Hendrickson | Developer + + + + +
+ +
+
+ + About Me +
+
+ + Projects +
+
+ + Resume +
+
+ + Contact +
+
+ + Terminal +
+
+ + +
+ + +
+
+ + Start +
+
+
+ +
+
+
+ + + + + +
+

👋 Hey, I'm Greg

+

Developer, tinkerer, and builder of things that probably shouldn't exist but do anyway.

+

I enjoy making AI slop into cool websites and building tools that make life easier (or at least more interesting).

+

Current Focus

+ +
+ +
+

📁 Projects

+
+
+

🐚 ShellMate

+

SSH-based gaming platform. Play chess, puzzles, and more right in your terminal.

+ shellmate.sh +
+
+

🤖 Clawdbot

+

AI assistant framework for automating everything.

+ GitHub +
+
+
+ +
+

📄 Resume

+

Experience

+
+

Developer & Tinkerer

+

Building cool shit since forever

+
+

Skills

+
+ Python + TypeScript + Go + Linux + Docker + AI/ML +
+
+ +
+

📬 Contact

+

Want to chat? Here's how to reach me:

+ +
+ +
+
+

Welcome to GregOS v1.0

+

Type 'help' for available commands.

+
+
+ guest@gregos:~$ + +
+
+ + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..60c18cf --- /dev/null +++ b/js/app.js @@ -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(); +});