Initial commit: Web development playground
- 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 <noreply@anthropic.com>
This commit is contained in:
commit
94e6b88cbe
11 changed files with 963 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
@ -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
|
30
CLAUDE.md
Normal file
30
CLAUDE.md
Normal file
|
@ -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.
|
83
ajax.html
Normal file
83
ajax.html
Normal file
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AJAX Demo</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>AJAX Demo</h1>
|
||||
<p>This page demonstrates asynchronous loading. The UI loads immediately, then makes a slow API call in the background.</p>
|
||||
|
||||
<div class="section">
|
||||
<h3>Slow Response Test</h3>
|
||||
<p>Click the button to make a request to the slow endpoint (1 second delay):</p>
|
||||
|
||||
<button id="loadBtn" onclick="loadSlowResponse()">Load Slow Response</button>
|
||||
<button id="clearBtn" onclick="clearResponse()">Clear</button>
|
||||
|
||||
<div id="responseField" class="response-field centered">
|
||||
<span class="loading">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div id="timestamp" class="timestamp"></div>
|
||||
|
||||
<div class="info">
|
||||
The page loads instantly and automatically starts loading the slow response. The API response takes ~1 second to arrive.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let requestStartTime;
|
||||
|
||||
async function loadSlowResponse() {
|
||||
const responseField = document.getElementById('responseField');
|
||||
const loadBtn = document.getElementById('loadBtn');
|
||||
const timestampDiv = document.getElementById('timestamp');
|
||||
|
||||
// Disable button and show loading state
|
||||
loadBtn.disabled = true;
|
||||
responseField.innerHTML = '<span class="loading">Loading... (this takes ~1 second)</span>';
|
||||
|
||||
requestStartTime = Date.now();
|
||||
timestampDiv.innerHTML = `Request started at: ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/slowtest');
|
||||
const data = await response.text();
|
||||
|
||||
const elapsed = Date.now() - requestStartTime;
|
||||
|
||||
responseField.innerHTML = `<span class="response large">Response: ${data}</span>`;
|
||||
timestampDiv.innerHTML = `Request completed at: ${new Date().toLocaleTimeString()} (took ${elapsed}ms)`;
|
||||
|
||||
} catch (error) {
|
||||
responseField.innerHTML = '<span class="error">Error loading response</span>';
|
||||
timestampDiv.innerHTML = `Error at: ${new Date().toLocaleTimeString()}`;
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
loadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearResponse() {
|
||||
const responseField = document.getElementById('responseField');
|
||||
const timestampDiv = document.getElementById('timestamp');
|
||||
|
||||
responseField.innerHTML = '<span class="loading">Click "Load Slow Response" to test the API</span>';
|
||||
timestampDiv.innerHTML = '';
|
||||
}
|
||||
|
||||
// Show that the page loaded immediately and start the slow request
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Page loaded immediately at:', new Date().toLocaleTimeString());
|
||||
loadSlowResponse();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
82
echo-test.html
Normal file
82
echo-test.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Echo Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Echo Test</h1>
|
||||
<p>Enter some text and click submit to test the echo API endpoint.</p>
|
||||
|
||||
<div class="section">
|
||||
<form id="echoForm">
|
||||
<div class="form-group">
|
||||
<label for="textInput">Enter your text:</label>
|
||||
<input type="text" id="textInput" name="text" placeholder="Type something here..." required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn">Submit</button>
|
||||
<button type="button" onclick="clearResponse()">Clear</button>
|
||||
</form>
|
||||
|
||||
<div id="responseField" class="response-field">
|
||||
Response will appear here...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('echoForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const textInput = document.getElementById('textInput');
|
||||
const responseField = document.getElementById('responseField');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
const text = textInput.value.trim();
|
||||
|
||||
if (!text) {
|
||||
responseField.innerHTML = '<span class="error">Please enter some text</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
submitBtn.disabled = true;
|
||||
responseField.innerHTML = '<span class="loading">Sending...</span>';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('text', text);
|
||||
|
||||
const response = await fetch('/api/echo', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.text();
|
||||
responseField.innerHTML = `<span class="response">${data}</span>`;
|
||||
} else {
|
||||
responseField.innerHTML = '<span class="error">Error: ' + response.status + '</span>';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
responseField.innerHTML = '<span class="error">Network error occurred</span>';
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function clearResponse() {
|
||||
document.getElementById('responseField').innerHTML = 'Response will appear here...';
|
||||
document.getElementById('textInput').value = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
17
go.mod
Normal file
17
go.mod
Normal file
|
@ -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
|
||||
)
|
23
go.sum
Normal file
23
go.sum
Normal file
|
@ -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=
|
32
index.html
Normal file
32
index.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Playground - Feature Showcase</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Playground - Feature Showcase</h1>
|
||||
<p>Welcome to the development playground! Click on any feature below to test it:</p>
|
||||
|
||||
<ul class="feature-list">
|
||||
<li>
|
||||
<a href="random.html">Random Number Generator</a>
|
||||
<div class="description">Displays a random number and allows you to generate new ones with a button click.</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="ajax.html">AJAX Demo</a>
|
||||
<div class="description">Demonstrates asynchronous loading with immediate UI and delayed API response (1 second delay).</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="echo-test.html">Echo Test</a>
|
||||
<div class="description">Text input form that sends data to the /echo API endpoint and displays the response.</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="items.html">Items Management</a>
|
||||
<div class="description">Full CRUD demo with SQLite database - create, view, and delete items with persistent storage.</div>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
149
items.html
Normal file
149
items.html
Normal file
|
@ -0,0 +1,149 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Items Management - Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Items Management Test</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>Add New Item</h2>
|
||||
<div class="form-group">
|
||||
<input type="text" id="itemName" placeholder="Enter item name" maxlength="100">
|
||||
<button onclick="createItem()">Add Item</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Items List</h2>
|
||||
<div id="itemsList" class="items-container">
|
||||
<p class="loading">Loading items...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<a href="index.html" class="back-button">← Back to Index</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let items = [];
|
||||
|
||||
// Load items when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadItems();
|
||||
});
|
||||
|
||||
// Load all items from API
|
||||
async function loadItems() {
|
||||
try {
|
||||
const response = await fetch('/api/items');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch items');
|
||||
}
|
||||
items = await response.json();
|
||||
displayItems();
|
||||
} catch (error) {
|
||||
console.error('Error loading items:', error);
|
||||
document.getElementById('itemsList').innerHTML = '<p class="error">Failed to load items</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Display items in the list
|
||||
function displayItems() {
|
||||
const itemsList = document.getElementById('itemsList');
|
||||
|
||||
if (items.length === 0) {
|
||||
itemsList.innerHTML = '<p class="empty">No items found. Add some items to get started!</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
items.forEach(item => {
|
||||
html += `
|
||||
<div class="item-row">
|
||||
<span class="item-name">${escapeHtml(item.name)}</span>
|
||||
<span class="item-id">#${item.id}</span>
|
||||
<button onclick="deleteItem(${item.id})" class="delete-btn">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
itemsList.innerHTML = html;
|
||||
}
|
||||
|
||||
// Create new item
|
||||
async function createItem() {
|
||||
const nameInput = document.getElementById('itemName');
|
||||
const name = nameInput.value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter an item name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/items', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name: name })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create item');
|
||||
}
|
||||
|
||||
nameInput.value = '';
|
||||
loadItems(); // Reload the list
|
||||
} catch (error) {
|
||||
console.error('Error creating item:', error);
|
||||
alert('Failed to create item: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete item
|
||||
async function deleteItem(id) {
|
||||
if (!confirm('Are you sure you want to delete this item?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/items/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to delete item');
|
||||
}
|
||||
|
||||
loadItems(); // Reload the list
|
||||
} catch (error) {
|
||||
console.error('Error deleting item:', error);
|
||||
alert('Failed to delete item: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Enter key in input field
|
||||
document.getElementById('itemName').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
createItem();
|
||||
}
|
||||
});
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
226
main.go
Normal file
226
main.go
Normal file
|
@ -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"})
|
||||
}
|
34
random.html
Normal file
34
random.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Random Number Generator</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<h1>Random Number Generator</h1>
|
||||
<div id="randomNumber" class="display-large">Loading...</div>
|
||||
<button class="btn-large" onclick="getNewNumber()">Get New Random Number</button>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function getNewNumber() {
|
||||
try {
|
||||
const response = await fetch('/api/randomnumber');
|
||||
const number = await response.text();
|
||||
document.getElementById('randomNumber').textContent = number;
|
||||
} catch (error) {
|
||||
document.getElementById('randomNumber').textContent = 'Error loading number';
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial number when page opens
|
||||
getNewNumber();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
266
styles.css
Normal file
266
styles.css
Normal file
|
@ -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;
|
||||
}
|
Loading…
Add table
Reference in a new issue