From 94e6b88cbe12f24d018bcebe23dae2e2a6fd9606 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 14 Jun 2025 01:38:32 +0200 Subject: [PATCH] Initial commit: Web development playground MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Frontend: HTML/CSS/JS demos and test pages - Backend: Go API with SQLite database - Features: AJAX calls, items management, random number generation - Database: SQLite with items table for CRUD operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 21 ++++ CLAUDE.md | 30 ++++++ ajax.html | 83 +++++++++++++++ echo-test.html | 82 +++++++++++++++ go.mod | 17 ++++ go.sum | 23 +++++ index.html | 32 ++++++ items.html | 149 +++++++++++++++++++++++++++ main.go | 226 +++++++++++++++++++++++++++++++++++++++++ random.html | 34 +++++++ styles.css | 266 +++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 963 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 ajax.html create mode 100644 echo-test.html create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.html create mode 100644 items.html create mode 100644 main.go create mode 100644 random.html create mode 100644 styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f74547 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Binaries +api + +# Database files +*.db + +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a1d2948 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +This is a test playground for web development - both frontend with simple html/css and vanilla javascript, and a backend API written in go. + +Currently it is not a git repo, it's just an experiment. + +**IMPORTANT: This is a learning project for a non-developer. Always prioritize maximum simplicity and readability over "best practices". Use the most straightforward, beginner-friendly approaches possible. Avoid complex patterns, frameworks, or advanced techniques.** + +The current purpose: develop all basic features necessary for a simple browsergame along the lines of ogame - no realtime unit controls, currently no 3d graphics, just a "planet status" screen with buildings and researches, some buttons that call the api, and a map. + +There is now a SQLite database available (`items.db`) with basic CRUD functionality. The database uses modernc.org/sqlite (pure Go, no CGO). Current tables: +- `items` table: id (INTEGER PRIMARY KEY), name (TEXT) - used for the items management demo + +remember to keep this file up to date if you change any of the points here. + +Current folder structure: flat. + +## API Access +There is a reverse proxy rewrite rule in place. To call any API endpoint from the frontend, you need to add `/api/` to the URL path: +- API endpoint URLs should be: `playground.shiny.space/api/yourendpointhere` +- Example: `/randomnumber` endpoint becomes `/api/randomnumber` + +## Development +To rebuild the Go API after making changes: +- Run `go build -o api` in the project directory +- The server has file watching enabled and will automatically restart with the new binary when it detects changes + +## Test Sites +The `index.html` file serves as the main navigation page listing all available test sites/demos. When creating new test HTML files, always add them to the feature list in `index.html` with a descriptive title and explanation. + +## Styling +All HTML pages use the unified `styles.css` file for consistent styling. Keep styling in this CSS file as much as reasonably possible - avoid inline styles in HTML files. It's perfectly fine to add new classes to `styles.css` for future tests and functionality. This approach keeps the test code much more readable and maintainable. diff --git a/ajax.html b/ajax.html new file mode 100644 index 0000000..bd71b70 --- /dev/null +++ b/ajax.html @@ -0,0 +1,83 @@ + + + + + + AJAX Demo + + + +

AJAX Demo

+

This page demonstrates asynchronous loading. The UI loads immediately, then makes a slow API call in the background.

+ +
+

Slow Response Test

+

Click the button to make a request to the slow endpoint (1 second delay):

+ + + + +
+ Loading... +
+ +
+ +
+ The page loads instantly and automatically starts loading the slow response. The API response takes ~1 second to arrive. +
+
+ + + + + + \ No newline at end of file diff --git a/echo-test.html b/echo-test.html new file mode 100644 index 0000000..7d2de50 --- /dev/null +++ b/echo-test.html @@ -0,0 +1,82 @@ + + + + + + Echo Test + + + +

Echo Test

+

Enter some text and click submit to test the echo API endpoint.

+ +
+
+
+ + +
+ + + +
+ +
+ Response will appear here... +
+
+ + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..752ac9e --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module api + +go 1.24.3 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sys v0.33.0 // indirect + modernc.org/libc v1.65.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9c471fd --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= +modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= diff --git a/index.html b/index.html new file mode 100644 index 0000000..503933b --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + + + Playground - Feature Showcase + + + +

Playground - Feature Showcase

+

Welcome to the development playground! Click on any feature below to test it:

+ + + + diff --git a/items.html b/items.html new file mode 100644 index 0000000..e6233db --- /dev/null +++ b/items.html @@ -0,0 +1,149 @@ + + + + + + Items Management - Test + + + +
+

Items Management Test

+ +
+

Add New Item

+
+ + +
+
+ +
+

Items List

+
+

Loading items...

+
+
+ + +
+ + + + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..2c1af6f --- /dev/null +++ b/main.go @@ -0,0 +1,226 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "time" + + _ "modernc.org/sqlite" +) + +type Item struct { + ID int `json:"id"` + Name string `json:"name"` +} + +var db *sql.DB + +func initDB() { + var err error + db, err = sql.Open("sqlite", "items.db") + if err != nil { + log.Fatal("Failed to open database:", err) + } + + createTable := ` + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + );` + + _, err = db.Exec(createTable) + if err != nil { + log.Fatal("Failed to create table:", err) + } +} + +func main() { + if len(os.Args) > 1 && os.Args[1] == "--watch" { + watchAndRestart() + return + } + + // Initialize database + initDB() + defer db.Close() + + // Your actual API + rand.Seed(time.Now().UnixNano()) + http.HandleFunc("/randomnumber", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "%d", rand.Intn(1000)) + }) + + http.HandleFunc("/slowtest", func(w http.ResponseWriter, r *http.Request) { + time.Sleep(1 * time.Second) + fmt.Fprintf(w, "true") + }) + + http.HandleFunc("/welcome", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "welcome to shinyspace play api testing") + }) + + http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + r.ParseMultipartForm(32 << 20) // 32MB max memory + text := r.FormValue("text") + fmt.Fprintf(w, "you said %s", text) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprintf(w, "Only POST method allowed") + } + }) + + // Items API endpoints + http.HandleFunc("/items", handleItems) + http.HandleFunc("/items/", handleItemDelete) + + log.Println("API running on :3847") + http.ListenAndServe("127.0.0.1:3847", nil) +} + +func watchAndRestart() { + binaryPath, _ := os.Executable() + var cmd *exec.Cmd + + start := func() { + if cmd != nil && cmd.Process != nil { + cmd.Process.Kill() + } + cmd = exec.Command(binaryPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Start() + } + + start() // Initial start + + lastMod := getModTime(binaryPath) + for { + time.Sleep(1 * time.Second) + if mod := getModTime(binaryPath); mod.After(lastMod) { + log.Println("Binary changed, restarting...") + lastMod = mod + start() + } + } +} + +func getModTime(path string) time.Time { + info, _ := os.Stat(path) + return info.ModTime() +} + +func handleItems(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case "GET": + getItems(w, r) + case "POST": + createItem(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"}) + } +} + +func getItems(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query("SELECT id, name FROM items ORDER BY id") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to fetch items"}) + return + } + defer rows.Close() + + var items []Item + for rows.Next() { + var item Item + err := rows.Scan(&item.ID, &item.Name) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to scan items"}) + return + } + items = append(items, item) + } + + // Ensure we always return an array, even if empty + if items == nil { + items = []Item{} + } + + json.NewEncoder(w).Encode(items) +} + +func createItem(w http.ResponseWriter, r *http.Request) { + var item Item + err := json.NewDecoder(r.Body).Decode(&item) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"}) + return + } + + if item.Name == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Name is required"}) + return + } + + result, err := db.Exec("INSERT INTO items (name) VALUES (?)", item.Name) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to create item"}) + return + } + + id, _ := result.LastInsertId() + item.ID = int(id) + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(item) +} + +func handleItemDelete(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method != "DELETE" { + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method not allowed"}) + return + } + + // Extract ID from URL path /items/{id} + path := strings.TrimPrefix(r.URL.Path, "/items/") + id, err := strconv.Atoi(path) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Invalid item ID"}) + return + } + + result, err := db.Exec("DELETE FROM items WHERE id = ?", id) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to delete item"}) + return + } + + rowsAffected, _ := result.RowsAffected() + if rowsAffected == 0 { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "Item not found"}) + return + } + + json.NewEncoder(w).Encode(map[string]string{"message": "Item deleted successfully"}) +} diff --git a/random.html b/random.html new file mode 100644 index 0000000..4272fba --- /dev/null +++ b/random.html @@ -0,0 +1,34 @@ + + + + + + Random Number Generator + + + +

Random Number Generator

+
Loading...
+ + + + + + + \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..a9bfedb --- /dev/null +++ b/styles.css @@ -0,0 +1,266 @@ +/* Base Layout */ +body { + font-family: Arial, sans-serif; + max-width: 700px; + margin: 50px auto; + padding: 20px; + line-height: 1.6; +} + +/* Typography */ +h1 { + color: #333; + text-align: center; + margin-bottom: 30px; +} + +h3 { + color: #333; + margin-top: 0; +} + +p { + color: #333; +} + +/* Buttons */ +button { + font-size: 16px; + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-right: 10px; +} + +button:hover { + background-color: #0056b3; +} + +button:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +/* Large button variant for special cases */ +.btn-large { + font-size: 18px; + padding: 12px 24px; +} + +/* Sections */ +.section { + margin: 30px 0; + padding: 20px; + border: 2px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; +} + +/* Feature List (for index page) */ +.feature-list { + list-style: none; + padding: 0; +} + +.feature-list li { + margin: 15px 0; + padding: 15px; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; +} + +.feature-list a { + text-decoration: none; + color: #007bff; + font-weight: bold; + font-size: 18px; +} + +.feature-list a:hover { + text-decoration: underline; +} + +.description { + color: #666; + margin-top: 8px; + font-size: 14px; +} + +/* Response Fields */ +.response-field { + min-height: 50px; + padding: 15px; + border: 2px solid #ccc; + border-radius: 4px; + background-color: white; + margin: 15px 0; + font-size: 16px; + word-wrap: break-word; +} + +/* Special display for large numbers */ +.display-large { + font-size: 48px; + font-weight: bold; + color: #333; + margin: 30px 0; + padding: 20px; + border: 2px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; + text-align: center; +} + +/* Centered response fields */ +.response-field.centered { + display: flex; + align-items: center; + justify-content: center; +} + +/* Forms */ +.form-group { + margin: 15px 0; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="text"] { + width: 100%; + padding: 10px; + border: 2px solid #ccc; + border-radius: 4px; + font-size: 16px; + box-sizing: border-box; +} + +/* Status Classes */ +.loading { + color: #666; + font-style: italic; +} + +.response { + color: #007bff; + font-weight: bold; +} + +.response.large { + font-size: 18px; +} + +.error { + color: #dc3545; +} + +/* Navigation */ +.back-link { + margin-top: 30px; + text-align: center; +} + +.back-link a { + color: #666; + text-decoration: none; +} + +.back-link a:hover { + text-decoration: underline; +} + +/* Utility Classes */ +.info { + color: #666; + font-size: 14px; + margin-top: 10px; +} + +.timestamp { + color: #999; + font-size: 12px; + margin-top: 5px; +} + +.text-center { + text-align: center; +} + +/* Items Management Styles */ +.container { + max-width: 800px; + margin: 0 auto; +} + +.items-container { + min-height: 100px; +} + +.item-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 15px; + margin: 8px 0; + border: 1px solid #ddd; + border-radius: 6px; + background-color: white; +} + +.item-name { + flex-grow: 1; + font-size: 16px; + color: #333; +} + +.item-id { + color: #666; + font-size: 14px; + margin-right: 15px; +} + +.delete-btn { + background-color: #dc3545; + padding: 6px 12px; + font-size: 14px; + margin: 0; +} + +.delete-btn:hover { + background-color: #c82333; +} + +.form-group { + display: flex; + gap: 10px; + align-items: center; +} + +.form-group input[type="text"] { + flex-grow: 1; + width: auto; +} + +.empty { + color: #666; + font-style: italic; + text-align: center; + padding: 20px; +} + +.back-button { + color: #666; + text-decoration: none; + font-size: 16px; +} + +.back-button:hover { + text-decoration: underline; +} \ No newline at end of file