Skip to content

Commit ea7b7a4

Browse files
davindicodeclaude
andcommitted
Initial commit: otgcode — on-the-go code
Mobile-first web app for controlling a VPS/local machine from anywhere. React Router v7 (framework mode) + Express + Socket.IO + node-pty. Features: - Browser terminal with xterm.js, multi-session tabs, font size control - Quick action key buttons (Ctrl combos, vim, nano, nav) - File explorer with multi-session tabs, breadcrumbs, context menu - Monaco code editor with markdown/HTML/Jupyter notebook preview - Localhost reverse proxy with browser tabs - Cloudflare quick tunnel support - Responsive: 3-column resizable panels (desktop), tab bar (mobile) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 parents  commit ea7b7a4

58 files changed

Lines changed: 3478 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.react-router
2+
build
3+
node_modules
4+
README.md

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.DS_Store
2+
.env
3+
/node_modules/
4+
pnpm-lock.yaml
5+
6+
# React Router
7+
/.react-router/
8+
/build/
9+
10+
# Editor / OS
11+
*.swp
12+
*.swo
13+
*~
14+
.vscode/
15+
.idea/
16+
Thumbs.db
17+
18+
# Logs
19+
*.log
20+
/tmp/

Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM node:20-alpine AS development-dependencies-env
2+
COPY . /app
3+
WORKDIR /app
4+
RUN npm ci
5+
6+
FROM node:20-alpine AS production-dependencies-env
7+
COPY ./package.json package-lock.json /app/
8+
WORKDIR /app
9+
RUN npm ci --omit=dev
10+
11+
FROM node:20-alpine AS build-env
12+
COPY . /app/
13+
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
14+
WORKDIR /app
15+
RUN npm run build
16+
17+
FROM node:20-alpine
18+
COPY ./package.json package-lock.json /app/
19+
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
20+
COPY --from=build-env /app/build /app/build
21+
WORKDIR /app
22+
CMD ["npm", "run", "start"]

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Welcome to React Router!
2+
3+
A modern, production-ready template for building full-stack React applications using React Router.
4+
5+
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
6+
7+
## Features
8+
9+
- 🚀 Server-side rendering
10+
- ⚡️ Hot Module Replacement (HMR)
11+
- 📦 Asset bundling and optimization
12+
- 🔄 Data loading and mutations
13+
- 🔒 TypeScript by default
14+
- 🎉 TailwindCSS for styling
15+
- 📖 [React Router docs](https://reactrouter.com/)
16+
17+
## Getting Started
18+
19+
### Installation
20+
21+
Install the dependencies:
22+
23+
```bash
24+
npm install
25+
```
26+
27+
### Development
28+
29+
Start the development server with HMR:
30+
31+
```bash
32+
npm run dev
33+
```
34+
35+
Your application will be available at `http://localhost:5173`.
36+
37+
## Building for Production
38+
39+
Create a production build:
40+
41+
```bash
42+
npm run build
43+
```
44+
45+
## Deployment
46+
47+
### Docker Deployment
48+
49+
To build and run using Docker:
50+
51+
```bash
52+
docker build -t my-app .
53+
54+
# Run the container
55+
docker run -p 3000:3000 my-app
56+
```
57+
58+
The containerized application can be deployed to any platform that supports Docker, including:
59+
60+
- AWS ECS
61+
- Google Cloud Run
62+
- Azure Container Apps
63+
- Digital Ocean App Platform
64+
- Fly.io
65+
- Railway
66+
67+
### DIY Deployment
68+
69+
If you're familiar with deploying Node applications, the built-in app server is production-ready.
70+
71+
Make sure to deploy the output of `npm run build`
72+
73+
```
74+
├── package.json
75+
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
76+
├── build/
77+
│ ├── client/ # Static assets
78+
│ └── server/ # Server-side code
79+
```
80+
81+
## Styling
82+
83+
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
84+
85+
---
86+
87+
Built with ❤️ using React Router.

app/app.css

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
@import "tailwindcss";
2+
3+
@theme {
4+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
5+
--font-mono: "JetBrains Mono", "Menlo", "Monaco", "Courier New", monospace;
6+
}
7+
8+
html,
9+
body {
10+
height: 100%;
11+
margin: 0;
12+
overflow: hidden;
13+
color-scheme: dark;
14+
}
15+
16+
/* Force the app shell to fill the viewport regardless of wrapper divs */
17+
.app-shell {
18+
position: fixed;
19+
inset: 0;
20+
display: flex;
21+
flex-direction: column;
22+
}
23+
24+
/* xterm.js overrides */
25+
.xterm {
26+
height: 100%;
27+
padding: 4px;
28+
}
29+
30+
.xterm-viewport {
31+
overflow-y: auto !important;
32+
}
33+
34+
/* Safe area for iOS bottom bar */
35+
.safe-bottom {
36+
padding-bottom: env(safe-area-inset-bottom, 0px);
37+
}
38+
39+
/* Loading spinner */
40+
.spinner {
41+
width: 32px;
42+
height: 32px;
43+
border: 3px solid #333;
44+
border-top-color: #569cd6;
45+
border-radius: 50%;
46+
animation: spin 0.8s linear infinite;
47+
}
48+
49+
@keyframes spin {
50+
to { transform: rotate(360deg); }
51+
}
52+
53+
/* Resizable panel dividers — wide touch target, thin visible line */
54+
.divider-handle {
55+
width: 12px;
56+
cursor: col-resize;
57+
display: flex;
58+
align-items: stretch;
59+
justify-content: center;
60+
touch-action: none;
61+
}
62+
63+
.divider-handle:hover .divider-line,
64+
.divider-handle:active .divider-line {
65+
background: #569cd6;
66+
}
67+
68+
.divider-line {
69+
width: 2px;
70+
background: #333;
71+
transition: background 0.15s;
72+
}
73+
74+
/* Hide scrollbar for breadcrumb bar */
75+
.scrollbar-none {
76+
-ms-overflow-style: none;
77+
scrollbar-width: none;
78+
}
79+
.scrollbar-none::-webkit-scrollbar {
80+
display: none;
81+
}
82+
83+
/* Prose styling for markdown/notebook preview */
84+
.prose {
85+
color: #d4d4d4;
86+
line-height: 1.7;
87+
}
88+
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
89+
color: #e0e0e0;
90+
margin-top: 1.2em;
91+
margin-bottom: 0.5em;
92+
font-weight: 600;
93+
}
94+
.prose h1 { font-size: 1.5em; border-bottom: 1px solid #333; padding-bottom: 0.3em; }
95+
.prose h2 { font-size: 1.3em; border-bottom: 1px solid #333; padding-bottom: 0.2em; }
96+
.prose h3 { font-size: 1.15em; }
97+
.prose p { margin: 0.6em 0; }
98+
.prose a { color: #569cd6; text-decoration: underline; }
99+
.prose a:hover { color: #7fbfff; }
100+
.prose strong { color: #e0e0e0; font-weight: 600; }
101+
.prose em { font-style: italic; }
102+
.prose code {
103+
background: #1e1e3a;
104+
padding: 0.15em 0.4em;
105+
border-radius: 3px;
106+
font-size: 0.88em;
107+
font-family: var(--font-mono);
108+
}
109+
.prose pre {
110+
background: #1a1a2e;
111+
border: 1px solid #333;
112+
border-radius: 6px;
113+
padding: 0.8em 1em;
114+
overflow-x: auto;
115+
margin: 0.8em 0;
116+
}
117+
.prose pre code {
118+
background: none;
119+
padding: 0;
120+
border-radius: 0;
121+
font-size: 0.85em;
122+
}
123+
.prose ul, .prose ol { padding-left: 1.5em; margin: 0.5em 0; }
124+
.prose li { margin: 0.25em 0; }
125+
.prose ul { list-style: disc; }
126+
.prose ol { list-style: decimal; }
127+
.prose blockquote {
128+
border-left: 3px solid #569cd6;
129+
padding-left: 1em;
130+
margin: 0.8em 0;
131+
color: #999;
132+
}
133+
.prose table { border-collapse: collapse; width: 100%; margin: 0.8em 0; }
134+
.prose th, .prose td {
135+
border: 1px solid #333;
136+
padding: 0.4em 0.8em;
137+
text-align: left;
138+
}
139+
.prose th { background: #1e1e3a; font-weight: 600; }
140+
.prose img { max-width: 100%; border-radius: 4px; }
141+
.prose hr { border: none; border-top: 1px solid #333; margin: 1.5em 0; }
142+
143+
/* Scrollbar styling */
144+
::-webkit-scrollbar {
145+
width: 6px;
146+
height: 6px;
147+
}
148+
149+
::-webkit-scrollbar-track {
150+
background: transparent;
151+
}
152+
153+
::-webkit-scrollbar-thumb {
154+
background: #333;
155+
border-radius: 3px;
156+
}
157+
158+
::-webkit-scrollbar-thumb:hover {
159+
background: #555;
160+
}

app/components/AppShell.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useState, useEffect } from "react";
2+
import Header from "./Header";
3+
import MobileTabBar from "./MobileTabBar";
4+
import ResizablePanels from "./ResizablePanels";
5+
import TerminalPage from "./terminal/TerminalPage";
6+
import FilesPage from "./files/FilesPage";
7+
import BrowserPage from "./browser/BrowserPage";
8+
import InputBox from "./terminal/InputBox";
9+
import { useUiStore } from "~/stores/uiStore";
10+
11+
function useIsDesktop() {
12+
const [isDesktop, setIsDesktop] = useState(
13+
typeof window !== "undefined" ? window.innerWidth >= 768 : true
14+
);
15+
useEffect(() => {
16+
const mq = window.matchMedia("(min-width: 768px)");
17+
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
18+
mq.addEventListener("change", handler);
19+
return () => mq.removeEventListener("change", handler);
20+
}, []);
21+
return isDesktop;
22+
}
23+
24+
export default function AppShell() {
25+
const activeTab = useUiStore((s) => s.activeTab);
26+
const isDesktop = useIsDesktop();
27+
28+
return (
29+
<div className="app-shell bg-[#0d0d1a] text-white">
30+
<Header />
31+
32+
{isDesktop ? (
33+
/* Desktop: 3-column resizable panels */
34+
<div className="flex flex-1 overflow-hidden min-h-0">
35+
<ResizablePanels
36+
left={<FilesPage />}
37+
center={
38+
<div className="flex flex-col h-full min-h-0 min-w-0 w-full">
39+
<TerminalPage />
40+
<InputBox />
41+
</div>
42+
}
43+
right={<BrowserPage />}
44+
/>
45+
</div>
46+
) : (
47+
/* Mobile: single panel with tab switching */
48+
<>
49+
<div className="flex flex-col flex-1 overflow-hidden min-h-0">
50+
<div
51+
className="flex-1 flex flex-col min-h-0"
52+
style={{ display: activeTab === "terminal" ? "flex" : "none" }}
53+
>
54+
<TerminalPage />
55+
<InputBox />
56+
</div>
57+
<div
58+
className="flex-1 flex flex-col min-h-0"
59+
style={{ display: activeTab === "files" ? "flex" : "none" }}
60+
>
61+
<FilesPage />
62+
</div>
63+
<div
64+
className="flex-1 flex flex-col min-h-0"
65+
style={{ display: activeTab === "browser" ? "flex" : "none" }}
66+
>
67+
<BrowserPage />
68+
</div>
69+
</div>
70+
<MobileTabBar />
71+
</>
72+
)}
73+
</div>
74+
);
75+
}

app/components/ClientOnly.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { useState, useEffect, type ReactNode } from "react";
2+
3+
export default function ClientOnly({ children }: { children: () => ReactNode }) {
4+
const [mounted, setMounted] = useState(false);
5+
useEffect(() => setMounted(true), []);
6+
return mounted ? <>{children()}</> : null;
7+
}

0 commit comments

Comments
 (0)