working on crud example
This commit is contained in:
parent
4120caf1b4
commit
3dd83793af
65
crud/.server/api/board.go
Normal file
65
crud/.server/api/board.go
Normal 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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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.
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html >
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@ -8,16 +8,22 @@
|
|||||||
<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>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
3
crud/.server/http/api.http
Normal file
3
crud/.server/http/api.http
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
###
|
||||||
|
GET http://localhost:8080/board
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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}}
|
||||||
9
experiments/patchsignals/datastar.js
Normal file
9
experiments/patchsignals/datastar.js
Normal file
File diff suppressed because one or more lines are too long
7
experiments/patchsignals/datastar.js.map
Normal file
7
experiments/patchsignals/datastar.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||||
|
|||||||
@ -31,5 +31,5 @@ body {
|
|||||||
|
|
||||||
div {
|
div {
|
||||||
color: var(--base-color-content);
|
color: var(--base-color-content);
|
||||||
font-size: 4rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user