From 5567158bbea9c955f3249d7aac2a31ce277090b0 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 11 Apr 2026 02:35:04 +0300 Subject: [PATCH] feat: initial test static site with Deno backend --- README.md | 26 +++++++++ pages/api/echo.ts | 30 +++++++++++ pages/api/hello.ts | 14 +++++ pages/api/time.ts | 10 ++++ pages/app.js | 44 +++++++++++++++ pages/index.html | 47 ++++++++++++++++ pages/style.css | 131 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 302 insertions(+) create mode 100644 README.md create mode 100644 pages/api/echo.ts create mode 100644 pages/api/hello.ts create mode 100644 pages/api/time.ts create mode 100644 pages/app.js create mode 100644 pages/index.html create mode 100644 pages/style.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2f9eae --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Test Static Site + +Test repository for Docker Watcher's Static Sites feature. + +## Structure + +``` +pages/ +├── index.html # Main page +├── style.css # Styles +├── app.js # Client-side JS +└── api/ + ├── time.ts # GET /api/time — server time + ├── hello.ts # GET /api/hello?name=X — greeting with env var + └── echo.ts # POST /api/echo — echo request body +``` + +## API Convention + +Functions prefixed with `API_` are auto-routed: +- `API_get` in `api/time.ts` → `GET /api/time` +- `API_post` in `api/echo.ts` → `POST /api/echo` + +## Environment Variables + +- `GREETING_PREFIX` — custom greeting prefix (default: "Hello") \ No newline at end of file diff --git a/pages/api/echo.ts b/pages/api/echo.ts new file mode 100644 index 0000000..aec8dc7 --- /dev/null +++ b/pages/api/echo.ts @@ -0,0 +1,30 @@ +// POST /api/echo — echoes back the request body with server metadata. +export async function API_post(req: Request): Promise { + let body: unknown = null; + + try { + body = await req.json(); + } catch { + return Response.json( + { error: "Invalid JSON body" }, + { status: 400 }, + ); + } + + return Response.json({ + echo: body, + server: { + time: new Date().toISOString(), + method: req.method, + headers: Object.fromEntries(req.headers.entries()), + }, + }); +} + +// GET /api/echo — returns usage instructions. +export async function API_get(req: Request): Promise { + return Response.json({ + usage: "POST a JSON body to this endpoint to see it echoed back.", + example: { text: "hello", timestamp: "2026-04-11T00:00:00Z" }, + }); +} \ No newline at end of file diff --git a/pages/api/hello.ts b/pages/api/hello.ts new file mode 100644 index 0000000..d3d13d2 --- /dev/null +++ b/pages/api/hello.ts @@ -0,0 +1,14 @@ +// GET /api/hello — returns a greeting. +// Demonstrates reading query parameters and using env vars. +export async function API_get(req: Request): Promise { + const url = new URL(req.url); + const name = url.searchParams.get("name") || "World"; + + // Example: reading a secret from environment (configured in Docker Watcher). + const prefix = Deno.env.get("GREETING_PREFIX") || "Hello"; + + return Response.json({ + message: `${prefix}, ${name}!`, + served_by: "Deno on Docker Watcher", + }); +} \ No newline at end of file diff --git a/pages/api/time.ts b/pages/api/time.ts new file mode 100644 index 0000000..9ff423d --- /dev/null +++ b/pages/api/time.ts @@ -0,0 +1,10 @@ +// GET /api/time — returns the current server time. +export async function API_get(req: Request): Promise { + const now = new Date(); + + return Response.json({ + time: now.toISOString(), + unix: Math.floor(now.getTime() / 1000), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); +} \ No newline at end of file diff --git a/pages/app.js b/pages/app.js new file mode 100644 index 0000000..1bab2de --- /dev/null +++ b/pages/app.js @@ -0,0 +1,44 @@ +async function fetchTime() { + const el = document.getElementById('server-time'); + el.textContent = 'Loading...'; + try { + const res = await fetch('/api/time'); + const data = await res.json(); + el.textContent = data.time; + } catch (e) { + el.textContent = 'Error: ' + e.message; + } +} + +async function fetchGreeting() { + const name = document.getElementById('name-input').value || 'World'; + const el = document.getElementById('greeting'); + el.textContent = 'Loading...'; + try { + const res = await fetch('/api/hello?name=' + encodeURIComponent(name)); + const data = await res.json(); + el.textContent = data.message; + } catch (e) { + el.textContent = 'Error: ' + e.message; + } +} + +async function fetchEcho() { + const input = document.getElementById('echo-input').value; + const el = document.getElementById('echo-result'); + el.textContent = 'Loading...'; + try { + const res = await fetch('/api/echo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: input, timestamp: new Date().toISOString() }) + }); + const data = await res.json(); + el.textContent = JSON.stringify(data, null, 2); + } catch (e) { + el.textContent = 'Error: ' + e.message; + } +} + +// Load server time on page load. +fetchTime(); \ No newline at end of file diff --git a/pages/index.html b/pages/index.html new file mode 100644 index 0000000..f7fc93c --- /dev/null +++ b/pages/index.html @@ -0,0 +1,47 @@ + + + + + + Test Static Site + + + +
+

Docker Watcher Static Sites

+

This page is served from a Gitea repository folder

+ +
+
+

Server Time

+

Loading...

+ +
+ +
+

Greeting

+
+ + +
+

+
+ +
+

Echo

+
+ + +
+

+      
+
+ +
+

Mode: Deno (Static + API) · Powered by Docker Watcher

+
+
+ + + + diff --git a/pages/style.css b/pages/style.css new file mode 100644 index 0000000..ca36f4d --- /dev/null +++ b/pages/style.css @@ -0,0 +1,131 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f172a; + color: #e2e8f0; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.container { + max-width: 800px; + width: 100%; + padding: 2rem; +} + +h1 { + font-size: 2rem; + background: linear-gradient(135deg, #818cf8, #c084fc); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +.subtitle { + color: #94a3b8; + margin-bottom: 2rem; +} + +.cards { + display: grid; + gap: 1.5rem; + grid-template-columns: 1fr; +} + +@media (min-width: 600px) { + .cards { + grid-template-columns: 1fr 1fr; + } + .cards .card:last-child { + grid-column: 1 / -1; + } +} + +.card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 12px; + padding: 1.5rem; +} + +.card h2 { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #94a3b8; + margin-bottom: 1rem; +} + +.value { + font-size: 1.125rem; + color: #f1f5f9; + margin-top: 0.75rem; + min-height: 1.5em; +} + +pre.value { + font-size: 0.8rem; + background: #0f172a; + padding: 0.75rem; + border-radius: 8px; + overflow-x: auto; + white-space: pre-wrap; +} + +.input-group { + display: flex; + gap: 0.5rem; +} + +input { + flex: 1; + padding: 0.5rem 0.75rem; + border: 1px solid #475569; + border-radius: 8px; + background: #0f172a; + color: #e2e8f0; + font-size: 0.875rem; +} + +input:focus { + outline: none; + border-color: #818cf8; +} + +button { + padding: 0.5rem 1rem; + border: none; + border-radius: 8px; + background: #6366f1; + color: white; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; +} + +button:hover { + background: #4f46e5; +} + +button:active { + background: #4338ca; +} + +footer { + margin-top: 2rem; + text-align: center; + color: #64748b; + font-size: 0.8rem; +} + +footer strong { + color: #a78bfa; +}