working on crud example

This commit is contained in:
thomashamburg 2025-11-09 11:34:11 +01:00
parent 4120caf1b4
commit 3dd83793af
12 changed files with 190 additions and 149 deletions

65
crud/.server/api/board.go Normal file
View File

@ -0,0 +1,65 @@
package api
import (
"context"
"crud/sqlite"
"html/template"
"net/http"
)
func Board(ctx context.Context, db *sqlite.Database, templ *template.Template) http.Handler {
// Implementation of Board handler
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
query := `SELECT u.id, u.name, u.username, u.email, u.phone, u.website, a.street, a.suite, a.zipcode, a.city, c.name as company, c.catch_phrase, c.bs
FROM user u
JOIN company c ON u.company_id = c.id
JOIN address a ON u.address_id = a.id;
`
w.Header().Set("Content-Type", "text/html")
// w.Header().Set("Content-Type", "application/json")
records, err := db.ReadRecords(ctx, query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create a data structure to pass to template
data := map[string]interface{}{
"records": records,
}
// Execute template with proper error handling
if err := templ.ExecuteTemplate(w, "board", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
},
)
}
func DeleteRecord(ctx context.Context, db *sqlite.Database, templ *template.Template) http.Handler {
// Implementation of DeleteRecord handler
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "Missing id parameter", http.StatusBadRequest)
return
}
if err := db.DeleteRecord(ctx, "users", "id", id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Redirect or respond with success
http.Redirect(w, r, "/board", http.StatusSeeOther)
},
)
}

View File

@ -1,98 +0,0 @@
package api
import (
"encoding/json"
"log"
"mime"
"net/http"
"net/url"
"strings"
)
// isJSONLike checks if a string looks like it might be JSON
func isJSONLike(s string) bool {
s = strings.TrimSpace(s)
return (strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}")) ||
(strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]"))
}
// RequestEcho represents the structure of our response
type RequestEcho struct {
URL string `json:"url"`
Method string `json:"method"`
Path string `json:"path"`
QueryParams url.Values `json:"query_params"`
Headers http.Header `json:"headers"` // Use http.Header for canonical keys
Body interface{} `json:"body"` // Can be structured JSON or raw string
BodyRaw string `json:"body_raw"` // Original body as string
RemoteAddr string `json:"remote_addr"`
ContentType string `json:"content_type"`
ContentLength int64 `json:"content_length"`
}
func EchoHandler() http.Handler
{
// Read body
bodyBytes := c.Body() // Fiber's way to get the body
bodyString := string(bodyBytes)
// Try to parse body as JSON
var parsedBody interface{}
contentTypeHeader := c.Get("Content-Type") // Get Content-Type header
if len(bodyBytes) > 0 {
mediaType, _, err := mime.ParseMediaType(contentTypeHeader)
// Only attempt JSON parsing if Content-Type is application/json or it looks like JSON
if (err == nil && mediaType == "application/json") || isJSONLike(bodyString) {
if jsonErr := json.Unmarshal(bodyBytes, &parsedBody); jsonErr != nil {
// If JSON parsing fails, parsedBody remains nil.
// It will be set to bodyString later if it's still nil.
parsedBody = nil
}
}
}
if parsedBody == nil && len(bodyBytes) > 0 {
// For non-JSON bodies or if JSON parsing failed, use the raw string
parsedBody = bodyString
}
// Query Parameters
queryParams, _ := url.ParseQuery(string(c.Request().URI().QueryString()))
// Headers - ensuring canonical keys like net/http.Header
headers := make(http.Header)
c.Context().Request.Header.VisitAll(func(key, value []byte) {
k := string(key)
v := string(value)
headers.Add(k, v) // http.Header.Add appends, and canonicalizes the key on first Set/Add
})
// Create our response structure
echo := RequestEcho{
URL: c.OriginalURL(), // Path and query string
Method: c.Method(),
Path: c.Path(),
QueryParams: queryParams,
Headers: headers,
Body: parsedBody,
BodyRaw: bodyString,
RemoteAddr: c.Context().RemoteAddr().String(), // IP and Port
ContentType: contentTypeHeader,
ContentLength: int64(c.Context().Request.Header.ContentLength()),
}
// Marshal to JSON and write response
jsonResponse, err := json.MarshalIndent(echo, "", " ")
if err != nil {
log.Printf("Error creating JSON response: %v", err)
return c.Status(http.StatusInternalServerError).SendString("Error creating JSON response")
}
c.Set("Content-Type", "application/json")
c.Status(http.StatusOK)
// Log the request to stdout
// fmt.Printf("Received %s request to %s\n", c.Method(), c.Path())
return c.Send(jsonResponse)
}

Binary file not shown.

View File

@ -1,4 +1,4 @@
<!DOCTYPE html > <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -8,14 +8,20 @@
<link rel="stylesheet" href="pico.blue.css"> <link rel="stylesheet" href="pico.blue.css">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<script type="module" src="datastar.js"></script> <script type="module" src="datastar.js"></script>
<style>
#board {
grid-template-columns: repeat(3, 1fr);
/* 3 columns */
}
</style>
</head> </head>
<body class="container" data-theme="light"> <body class="container-fluid" data-theme="light">
<h1 style="margin-top: 1em; text-align:center">The CRUD Example</h1> <h1 style="margin-top: 1em; text-align:center">The CRUD Example</h1>
<main class="grid" data-theme="dark"> <main data-theme="dark">
<div id="board" data-init="@get('board-content')"></div> <div id="board" data-init="@get('board')"></div>
</main> </main>
</body> </body>

View File

@ -0,0 +1,3 @@
###
GET http://localhost:8080/board

View File

@ -5,16 +5,12 @@ package main
import ( import (
"context" "context"
"crud/datastar"
"crud/sqlite" "crud/sqlite"
"crypto/rand"
"embed" "embed"
"encoding/hex"
"fmt" "fmt"
"html/template" "html/template"
"io/fs" "io/fs"
"log" "log"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -35,7 +31,7 @@ var frontend embed.FS
//go:embed templates //go:embed templates
var templates embed.FS var templates embed.FS
var templ = template.Must(template.ParseFS(templates, "templates/*.html")) var templatesCompiled = template.Must(template.ParseFS(templates, "templates/*.html"))
// main is the entry point of the application. // main is the entry point of the application.
// Its task is to check wether all execution conditions are fullfilled. // Its task is to check wether all execution conditions are fullfilled.
@ -87,7 +83,7 @@ func run(db *sqlite.Database, ctx context.Context) error {
} }
defer db.Close() defer db.Close()
server := NewServer(db) server := NewServer(ctx, db)
httpServer := &http.Server{ httpServer := &http.Server{
Addr: ":8080", Addr: ":8080",
@ -120,7 +116,7 @@ func run(db *sqlite.Database, ctx context.Context) error {
} }
// The NewServer constructor is responsible for all the top-level HTTP stuff that applies to all endpoints, like CORS, auth middleware, and logging: // The NewServer constructor is responsible for all the top-level HTTP stuff that applies to all endpoints, like CORS, auth middleware, and logging:
func NewServer(db *sqlite.Database) http.Handler { func NewServer(ctx context.Context, db *sqlite.Database) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
static, err := fs.Sub(frontend, "frontend") static, err := fs.Sub(frontend, "frontend")
@ -137,43 +133,13 @@ func NewServer(db *sqlite.Database) http.Handler {
fmt.Println("--- End of tree ---") fmt.Println("--- End of tree ---")
addRoutes( addRoutes(
ctx,
mux, mux,
db, db,
static, static,
// templates, templatesCompiled,
) )
mux.HandleFunc("GET /stream", func(w http.ResponseWriter, r *http.Request) {
ticker := time.NewTicker(1000 * time.Millisecond)
// original: defer ticker.Stop()
defer func() {
fmt.Println("defer executed")
ticker.Stop()
}()
sse := datastar.NewSSE(w, r)
for {
select {
case <-r.Context().Done():
slog.Debug("Client connection closed")
return
case <-ticker.C:
bytes := make([]byte, 3)
if _, err := rand.Read(bytes); err != nil {
slog.Error("Error generating random bytes: ", slog.String("error", err.Error()))
return
}
hexString := hex.EncodeToString(bytes)
frag := fmt.Sprintf(`<span id="feed" style="color:#%s;border:1px solid #%s;border-radius:0.25rem;padding:1rem;">%s</span>`, hexString, hexString, hexString)
sse.MergeFragments(frag)
}
}
})
var handler http.Handler = mux var handler http.Handler = mux
// handler = authMiddleware(handler) // handler = authMiddleware(handler)

View File

@ -3,7 +3,10 @@ package main
// This file is the one place in your application where all routes are listed. // This file is the one place in your application where all routes are listed.
import ( import (
"context"
"crud/api"
"crud/sqlite" "crud/sqlite"
"html/template"
"io/fs" "io/fs"
"net/http" "net/http"
) )
@ -11,13 +14,15 @@ import (
// addRoutes combines the URL endpoints with the applications's services // addRoutes combines the URL endpoints with the applications's services
// and dependencies and required middleware // and dependencies and required middleware
func addRoutes( func addRoutes(
ctx context.Context,
mux *http.ServeMux, mux *http.ServeMux,
database *sqlite.Database, database *sqlite.Database,
static fs.FS, static fs.FS,
// templ *template.Template, templ *template.Template,
) { ) {
mux.Handle("GET /", http.FileServer(http.FS(static))) mux.Handle("GET /", http.FileServer(http.FS(static)))
// mux.Handle("GET /tables", api.TableList(database)) mux.Handle("GET /board", api.Board(ctx, database, templ))
mux.Handle("GET /delete-record/{id}", api.DeleteRecord(ctx, database, templ))
// mux.Handle("GET /count", api.ProductCount(database)) // mux.Handle("GET /count", api.ProductCount(database))
// mux.Handle("GET /nutriments/{page}", api.DataNutriments(database, templ)) // mux.Handle("GET /nutriments/{page}", api.DataNutriments(database, templ))
// mux.Handle("GET /products/{page}", api.DataProducts(database, templ)) // mux.Handle("GET /products/{page}", api.DataProducts(database, templ))

View File

@ -0,0 +1,61 @@
{{define "board"}}
<div id="board" class="grid">
{{range .records}}
<article>
<header>
<h3>{{.name}}</h3>
<p>@{{.username}}</p>
</header>
<form inert>
<label>
Email
<input type="email" value="{{.email}}" />
</label>
<label>
Phone
<input type="tel" value="{{.phone}}" />
</label>
<label>
Website
<input type="url" value="{{.website}}" />
</label>
<label>
Company
<input type="text" value="{{.company}}" />
</label>
<fieldset role="group">
<label>
Street
<input type="text" value="{{.street}}" />
</label>
<label>
Suite
<input type="text" value="{{.suite}}" />
</label>
</fieldset>
<fieldset role="group">
<label>
City
<input type="text" value="{{.city}}" />
</label>
<label>
Zipcode
<input type="text" value="{{.zipcode}}" />
</label>
</fieldset>
</label>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('edit-record/{{.id}}')">Edit</button>
<button class="secondary" data-on:click="@get('delete-record/{{.id}}')">Delete</button>
</article>
{{end}}
</div>
{{end}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,15 +4,32 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" defer <script type="module" src="datastar.js"></script>
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-RC.6/bundles/datastar.js"></script>
<title>Document</title> <title>Document</title>
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
</head> </head>
<body> <body>
<div data-signals:count="'START'" data-text="$count"></div> <div data-signals:count="-1" data-text="$count" data-signals:baz="'hi, there'"></div>
<pre data-json-signals></pre>
<div data-signals:foo="$baz">
<input data-bind:foo value="bar" />
</div>
<div data-signals:foot="0">
<select data-bind:foot>
<option value="5">10</option>
</select>
</div>
<input type="file" data-bind:files multiple />
<div data-init__delay.5000ms="$someValue = 1"></div>
<button data-on:dblclick__once="$count = 0">Reset</button>
<div data-on-intersect="$intersected = true"></div>
<div data-on-interval__duration.4s="$counter++"></div>
<div data-on-signal-patch="console.log('A signal changed!', patch)"></div>
<article data-ref:foo></article>
<div>$foo is a reference to a <span data-text="$foo.tagName"></span> element</div>
<script> <script>
let counter = 0; let counter = 0;
@ -34,7 +51,7 @@
setInterval(() => { setInterval(() => {
patchSignals(counter); patchSignals(counter);
counter++; counter++;
}, 250); }, 2000);
</script> </script>
</body> </body>

View File

@ -31,5 +31,5 @@ body {
div { div {
color: var(--base-color-content); color: var(--base-color-content);
font-size: 4rem; font-size: 1rem;
} }