mirror of
https://github.com/ghndrx/webos.git
synced 2026-02-10 06:45:00 +00:00
Initial WebOS portfolio site
This commit is contained in:
36
.github/workflows/deploy.yml
vendored
Normal file
36
.github/workflows/deploy.yml
vendored
Normal 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
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
30
README.md
Normal file
30
README.md
Normal 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
382
css/style.css
Normal 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
136
index.html
Normal 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
299
js/app.js
Normal 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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user