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:
Lucas 2025-06-14 01:38:32 +02:00
commit 94e6b88cbe
11 changed files with 963 additions and 0 deletions

21
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}