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.
@ -8,14 +8,20 @@
|
||||
<link rel="stylesheet" href="pico.blue.css">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script type="module" src="datastar.js"></script>
|
||||
<style>
|
||||
#board {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
/* 3 columns */
|
||||
}
|
||||
</style>
|
||||
</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>
|
||||
|
||||
<main class="grid" data-theme="dark">
|
||||
<div id="board" data-init="@get('board-content')"></div>
|
||||
<main data-theme="dark">
|
||||
<div id="board" data-init="@get('board')"></div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"crud/datastar"
|
||||
"crud/sqlite"
|
||||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@ -35,7 +31,7 @@ var frontend embed.FS
|
||||
//go:embed templates
|
||||
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.
|
||||
// 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()
|
||||
|
||||
server := NewServer(db)
|
||||
server := NewServer(ctx, db)
|
||||
|
||||
httpServer := &http.Server{
|
||||
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:
|
||||
func NewServer(db *sqlite.Database) http.Handler {
|
||||
func NewServer(ctx context.Context, db *sqlite.Database) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
static, err := fs.Sub(frontend, "frontend")
|
||||
@ -137,43 +133,13 @@ func NewServer(db *sqlite.Database) http.Handler {
|
||||
fmt.Println("--- End of tree ---")
|
||||
|
||||
addRoutes(
|
||||
ctx,
|
||||
mux,
|
||||
db,
|
||||
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
|
||||
|
||||
// handler = authMiddleware(handler)
|
||||
|
||||
@ -3,7 +3,10 @@ package main
|
||||
// This file is the one place in your application where all routes are listed.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crud/api"
|
||||
"crud/sqlite"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
@ -11,13 +14,15 @@ import (
|
||||
// addRoutes combines the URL endpoints with the applications's services
|
||||
// and dependencies and required middleware
|
||||
func addRoutes(
|
||||
ctx context.Context,
|
||||
mux *http.ServeMux,
|
||||
database *sqlite.Database,
|
||||
static fs.FS,
|
||||
// templ *template.Template,
|
||||
templ *template.Template,
|
||||
) {
|
||||
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 /nutriments/{page}", api.DataNutriments(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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script type="module" defer
|
||||
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-RC.6/bundles/datastar.js"></script>
|
||||
<script type="module" src="datastar.js"></script>
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
|
||||
<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>
|
||||
let counter = 0;
|
||||
|
||||
@ -34,7 +51,7 @@
|
||||
setInterval(() => {
|
||||
patchSignals(counter);
|
||||
counter++;
|
||||
}, 250);
|
||||
}, 2000);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -31,5 +31,5 @@ body {
|
||||
|
||||
div {
|
||||
color: var(--base-color-content);
|
||||
font-size: 4rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user