lot of breaking changes, checking in before stepping away

This commit is contained in:
2024-03-03 16:08:12 +00:00
parent 9b2caddeb4
commit be28d18723
12 changed files with 305 additions and 124 deletions

View File

@@ -20,9 +20,33 @@ const (
Other Other
) )
// entities var CategoryMap = map[Category]string{
type Item struct { Bedroom: "Bedroom",
Bathroom: "Bathroom",
Kitchen: "Kitchen",
Office: "Office",
LivingRoom: "Living Room",
Other: "Other",
}
var PackingStageMap = map[PackingStage]string{
Essentials: "Essentials",
StageOne: "Stage One",
StageTwo: "Stage Two",
StageThree: "Stage Three",
}
type EntityLabel string
const (
ItemType EntityLabel = "items"
BoxType EntityLabel = "boxes"
BoxItemType EntityLabel = "box_items"
)
type Entity struct {
ID int ID int
EntityLabel EntityLabel
Name string Name string
Notes *string Notes *string
Description *string Description *string
@@ -30,14 +54,8 @@ type Item struct {
Category Category Category Category
} }
type Box struct { type Item Entity
ID int type Box Entity
Name string
Notes *string
Description *string
Stage PackingStage
Category Category
}
// joins // joins
type BoxItem struct { type BoxItem struct {

View File

@@ -58,12 +58,12 @@ func GetSeedData() (items []Item, boxes []Box, boxitems []BoxItem) {
func CreateTables(client *sql.DB) (int64, error) { func CreateTables(client *sql.DB) (int64, error) {
script, err := os.ReadFile("/home/mikayla/go/go-htmx-tailwind-example/db/seed.sql") script, err := os.ReadFile("/home/mikayla/go/go-htmx-tailwind-example/db/seed.sql")
if err != nil { if err != nil {
panic(err) return -1, err
} }
result, err := client.Exec(string(script)) result, err := client.Exec(string(script))
if err != nil { if err != nil {
panic(err) return -1, err
} }
return result.RowsAffected() return result.RowsAffected()
@@ -86,6 +86,11 @@ func SeedDB() (int64, error) {
for i := range(items) { for i := range(items) {
_, err := PostItem(items[i]) _, err := PostItem(items[i])
if err != nil { if err != nil {
// ignore unique constraint violations and continue
if err.Error() == "UNIQUE constraint failed: items.Name" {
continue
}
return -1, err return -1, err
} }
insertCount++ insertCount++
@@ -94,6 +99,11 @@ func SeedDB() (int64, error) {
for i := range(boxes) { for i := range(boxes) {
_, err := PostBox(boxes[i]) _, err := PostBox(boxes[i])
if err != nil { if err != nil {
// ignore unique constraint violations and continue
if err.Error() == "UNIQUE constraint failed: boxes.Name" {
continue
}
return -1, err return -1, err
} }
insertCount++ insertCount++

157
db/sql.go
View File

@@ -2,13 +2,33 @@ package db
import ( import (
"database/sql" "database/sql"
"encoding/json"
) )
func CreateClient() (db *sql.DB, err error) { func CreateClient() (db *sql.DB, err error) {
return sql.Open("sqlite3", "./example.db") return sql.Open("sqlite3", "./example.db")
} }
func GetAllItems() (result []Item, err error) { func GetAllItems() (rows *sql.Rows, err error) {
db, err := CreateClient()
if err != nil {
return
}
defer db.Close()
rows, err = db.Query("SELECT * FROM items")
if err != nil {
return
}
// fmt.Println("rows", rows)
defer rows.Close()
return
}
func GetAll(table EntityLabel) (rows *sql.Rows, err error) {
db, err := CreateClient() db, err := CreateClient()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -16,46 +36,50 @@ func GetAllItems() (result []Item, err error) {
defer db.Close() defer db.Close()
rows, err := db.Query("SELECT * FROM items") rows, err = db.Query("SELECT * FROM ?", table)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
return
}
for rows.Next() { func GetByID(table EntityLabel, id int) (row *sql.Row, err error) {
item := Item{} db, err := CreateClient()
if err != nil {
return nil, err
}
err = rows.Scan(&item.ID, &item.Name, &item.Notes, &item.Description, &item.Stage, &item.Category) defer db.Close()
row = db.QueryRow("SELECT * FROM ? WHERE id = ?", table, id)
return
}
func Put[T Entity](table EntityLabel, record Entity) (sql.Result, error) {
db, err := CreateClient()
if err != nil {
return nil, err
}
defer db.Close()
query := `UPDATE ? SET name = ?, notes = ?, description = ?, stage = ?, category = ? WHERE id = ?`
result, err := db.Exec(query, table, record.Name, record.Notes, record.Description, record.Stage, record.Category, record.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result = append(result, item) return result, nil
}
return
} }
func GetItemByID(id int) (item Item, err error) { func PostItem(record Item) (sql.Result, error) {
db, err := CreateClient() db, err := CreateClient()
if err != nil { if err != nil {
return Item{}, err return nil, err
}
defer db.Close()
row := db.QueryRow("SELECT * FROM items WHERE id = ?", id)
err = row.Scan(&item.ID, &item.Name, &item.Notes, &item.Description, &item.Stage, &item.Category)
return
}
func PostItem(item Item) (int64, error) {
db, err := CreateClient()
if err != nil {
return -1, err
} }
defer db.Close() defer db.Close()
@@ -63,37 +87,16 @@ func PostItem(item Item) (int64, error) {
query := `INSERT INTO items (name, notes, description, stage, category) query := `INSERT INTO items (name, notes, description, stage, category)
VALUES (?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?)`
result, err := db.Exec(query, item.Name, item.Notes, item.Description, item.Stage, item.Category) result, err := db.Exec(query, record.Name, record.Notes, record.Description, record.Stage, record.Category)
if err != nil { if err != nil {
return -1, err return nil, err
} }
return result.LastInsertId() return result, nil
} }
func PostBox(record Box) (sql.Result, error) {
func PostBox(box Box) (int64, error) {
db, err := CreateClient()
if err != nil {
return -1, err
}
defer db.Close()
query := `INSERT INTO boxes (name, notes, description, stage, category) VALUES (?, ?, ?, ?, ?)`
result, err := db.Exec(query, box.Name, box.Notes, box.Description, box.Stage, box.Category)
if err != nil {
return -1, err
}
return result.LastInsertId()
}
func GetAllBoxes() (result []Box, err error) {
db, err := CreateClient() db, err := CreateClient()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -101,34 +104,50 @@ func GetAllBoxes() (result []Box, err error) {
defer db.Close() defer db.Close()
rows, err := db.Query("SELECT * FROM boxes") query := `INSERT INTO boxes (name, notes, description, stage, category)
if err != nil { VALUES (?, ?, ?, ?, ?)`
return nil, err
}
defer rows.Close() result, err := db.Exec(query, record.Name, record.Notes, record.Description, record.Stage, record.Category)
for rows.Next() {
box := Box{}
err = rows.Scan(&box.ID, &box.Name, &box.Notes, &box.Description, &box.Stage, &box.Category)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result = append(result, box) return result, nil
}
func Delete(table EntityLabel, id int) (sql.Result, error) {
db, err := CreateClient()
if err != nil {
return nil, err
} }
defer db.Close()
query := `DELETE FROM ? WHERE id = ?`
return db.Exec(query, table, id)
}
func ParseItem(item *Item, scan func(dest ...any) error) (err error) {
return scan(&item.ID, &item.Name, &item.Notes, &item.Description, &item.Stage, &item.Category)
}
func ParseBox(box *Box, scan func(dest ...any) error) error {
return scan(&box.ID, &box.Name, &box.Notes, &box.Description, &box.Stage, &box.Category)
}
func ParseEntityFromBytes(b []byte) (entity Entity, err error) {
err = json.Unmarshal(b, &entity)
return return
} }
// func PostBoxItem(itemid int, boxid int) (int64, error) { func ParseItemFromBytes(b []byte) (item Item, err error) {
// db, err := CreateClient() err = json.Unmarshal(b, &item)
// if err != nil { return
// return -1, err }
// }
// defer db.Close() func ParseBoxFromBytes(b []byte) (box Box, err error) {
// // query := err = json.Unmarshal(b, &box)
// } return
}

12
main.go
View File

@@ -34,10 +34,14 @@ func main() {
//exit process immediately upon sigterm //exit process immediately upon sigterm
handleSigTerms() handleSigTerms()
db.SeedDB() i, err := db.SeedDB()
if err != nil {
panic(err)
}
fmt.Printf("seeded db with %d records\n", i)
//parse templates //parse templates
var err error
html, err = web.TemplateParseFSRecursive(templateFS, ".html", true, nil) html, err = web.TemplateParseFSRecursive(templateFS, ".html", true, nil)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -52,7 +56,11 @@ func main() {
router.Handle("/", web.Action(routes.HomePage)) router.Handle("/", web.Action(routes.HomePage))
router.Handle("/items", web.Action(routes.Items(html).GetAll)) router.Handle("/items", web.Action(routes.Items(html).GetAll))
router.Handle("/items/{id}", web.Action(routes.Items(html).GetByID))
router.Handle("/boxes", web.Action(routes.Boxes(html).GetAll)) router.Handle("/boxes", web.Action(routes.Boxes(html).GetAll))
router.Handle("/unrelated", web.Action(func(r *http.Request) *web.Response {
return web.HTML(http.StatusOK, html, "row-edit.html", nil, nil)
}))
//logging/tracing //logging/tracing
nextRequestID := func() string { nextRequestID := func() string {

View File

@@ -3,17 +3,10 @@ package routes
import ( import (
"net/http" "net/http"
"github.com/innocuous-symmetry/moving-mgmt/db"
"github.com/jritsema/gotoolbox/web" "github.com/jritsema/gotoolbox/web"
) )
type Entity int
const (
Item Entity = iota
Box
BoxItem
)
type RouterActions struct { type RouterActions struct {
GetAll func(r *http.Request) *web.Response GetAll func(r *http.Request) *web.Response
GetByID func(r *http.Request) *web.Response GetByID func(r *http.Request) *web.Response
@@ -23,8 +16,7 @@ type RouterActions struct {
} }
type Router struct { type Router struct {
Entity Entity Entity db.EntityLabel
Path string
GetAll func(r *http.Request) *web.Response GetAll func(r *http.Request) *web.Response
GetByID func(r *http.Request) *web.Response GetByID func(r *http.Request) *web.Response
Post func(r *http.Request) *web.Response Post func(r *http.Request) *web.Response
@@ -32,10 +24,9 @@ type Router struct {
Delete func(r *http.Request) *web.Response Delete func(r *http.Request) *web.Response
} }
func NewRouter(entity Entity, path string, actions RouterActions) *Router { func NewRouter(entity db.EntityLabel, actions RouterActions) *Router {
return &Router{ return &Router{
Entity: entity, Entity: entity,
Path: path,
GetAll: actions.GetAll, GetAll: actions.GetAll,
GetByID: actions.GetByID, GetByID: actions.GetByID,
Post: actions.Post, Post: actions.Post,

View File

@@ -12,8 +12,7 @@ func Boxes(_html *template.Template) *Router {
html = _html html = _html
return NewRouter( return NewRouter(
Box, "boxes",
"/boxes",
RouterActions{ RouterActions{
GetAll: GetAllBoxes, GetAll: GetAllBoxes,
GetByID: nil, GetByID: nil,
@@ -25,11 +24,23 @@ func Boxes(_html *template.Template) *Router {
} }
func GetAllBoxes(_ *http.Request) *web.Response { func GetAllBoxes(_ *http.Request) *web.Response {
result, err := db.GetAllBoxes() result, err := db.GetAll("boxes")
if err != nil { if err != nil {
return web.Error(http.StatusBadRequest, err, nil) return web.Error(http.StatusBadRequest, err, nil)
} }
boxes := []db.Box{}
for result.Next() {
box := db.Box{}
err = db.ParseBox(&box, result.Scan)
if err != nil {
return web.Error(http.StatusInternalServerError, err, nil)
}
boxes = append(boxes, box)
}
return web.HTML( return web.HTML(
http.StatusOK, http.StatusOK,
html, html,

View File

@@ -1,8 +1,10 @@
package routes package routes
import ( import (
"fmt"
"html/template" "html/template"
"net/http" "net/http"
// "github.com/innocuous-symmetry/moving-mgmt/" // "github.com/innocuous-symmetry/moving-mgmt/"
db "github.com/innocuous-symmetry/moving-mgmt/db" db "github.com/innocuous-symmetry/moving-mgmt/db"
"github.com/jritsema/gotoolbox/web" "github.com/jritsema/gotoolbox/web"
@@ -12,15 +14,32 @@ var html *template.Template
func HomePage(r *http.Request) *web.Response { func HomePage(r *http.Request) *web.Response {
result, err := db.GetAllItems() result, err := db.GetAllItems()
fmt.Println(result)
if err != nil { if err != nil {
panic(err) return web.Error(http.StatusNotFound, err, nil)
}
items := []db.Item{}
for result.Next() {
item := db.Item{}
err = db.ParseItem(&item, result.Scan)
fmt.Println("name", item.Name)
if err != nil {
return web.Error(http.StatusInternalServerError, err, nil)
}
items = append(items, item)
} }
return web.HTML( return web.HTML(
http.StatusOK, http.StatusOK,
html, html,
"index.html", "index.html",
result, items,
nil, nil,
) )
} }

View File

@@ -1,11 +1,13 @@
package routes package routes
import ( import (
"fmt"
"html/template" "html/template"
"net/http" "net/http"
"strconv" "strconv"
db "github.com/innocuous-symmetry/moving-mgmt/db" db "github.com/innocuous-symmetry/moving-mgmt/db"
"github.com/innocuous-symmetry/moving-mgmt/util"
"github.com/jritsema/gotoolbox/web" "github.com/jritsema/gotoolbox/web"
) )
@@ -13,11 +15,10 @@ func Items(_html *template.Template) *Router {
html = _html html = _html
return NewRouter( return NewRouter(
Item, "items",
"/items",
RouterActions{ RouterActions{
GetAll: GetAllItems, GetAll: GetAllItems,
GetByID: nil, GetByID: GetItemByID,
Post: nil, Post: nil,
Put: nil, Put: nil,
Delete: nil, Delete: nil,
@@ -26,11 +27,26 @@ func Items(_html *template.Template) *Router {
} }
func GetAllItems(_ *http.Request) *web.Response { func GetAllItems(_ *http.Request) *web.Response {
result, err := db.GetAllItems() result, err := db.GetAll("items")
if err != nil { if err != nil {
panic(err) return web.Error(http.StatusNotFound, err, nil)
} }
items := []db.Item{}
for result.Next() {
item := db.Item{}
err = db.ParseItem(&item, result.Scan)
if err != nil {
fmt.Println(err.Error())
return web.Error(http.StatusInternalServerError, err, nil)
}
items = append(items, item)
}
fmt.Println("items", items)
return web.HTML( return web.HTML(
http.StatusOK, http.StatusOK,
html, html,
@@ -41,14 +57,57 @@ func GetAllItems(_ *http.Request) *web.Response {
} }
func GetItemByID(r *http.Request) *web.Response { func GetItemByID(r *http.Request) *web.Response {
var id int id, err := util.GetIDFromPath(r)
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil { if err != nil {
return web.Error(http.StatusBadRequest, err, nil) return web.Error(http.StatusBadRequest, err, nil)
} }
result, err := db.GetItemByID(id) editMode, err := strconv.ParseBool(r.URL.Query().Get("edit"))
if err != nil {
return web.Error(http.StatusBadRequest, err, nil)
}
res, err := db.GetByID("items", id)
item := db.Item{}
err = db.ParseItem(&item, res.Scan)
if err != nil {
return web.Error(http.StatusInternalServerError, err, nil)
}
var tmpl string
if editMode {
tmpl = "entity-edit.html"
} else {
tmpl = "entity-row.html"
}
return web.HTML(
http.StatusOK,
html,
tmpl,
item,
nil,
)
}
func PutItem(r *http.Request) *web.Response {
body := r.Body
defer body.Close()
bodyBytes := make([]byte, r.ContentLength)
_, err := body.Read(bodyBytes)
if err != nil {
return web.Error(http.StatusInternalServerError, err, nil)
}
item, err := db.ParseEntityFromBytes(bodyBytes)
if err != nil {
return web.Error(http.StatusBadRequest, err, nil)
}
result, err := db.Put("items", item)
if err != nil { if err != nil {
return web.Error(http.StatusInternalServerError, err, nil) return web.Error(http.StatusInternalServerError, err, nil)
} }
@@ -56,7 +115,7 @@ func GetItemByID(r *http.Request) *web.Response {
return web.HTML( return web.HTML(
http.StatusOK, http.StatusOK,
html, html,
"item-by-id.html", "entity-row.html",
result, result,
nil, nil,
) )

View File

@@ -5,4 +5,14 @@
<td class="whitespace-nowrap px-6 py-4">{{.Category}}</td> <td class="whitespace-nowrap px-6 py-4">{{.Category}}</td>
<td class="whitespace-nowrap px-6 py-4">{{.Description}}</td> <td class="whitespace-nowrap px-6 py-4">{{.Description}}</td>
<td class="whitespace-nowrap px-6 py-4">{{.Notes}}</td> <td class="whitespace-nowrap px-6 py-4">{{.Notes}}</td>
<td class="whitespace-nowrap px-6 py-4">
<button
hx-get="/items/{{.ID}}"
hx-params="edit=true"
hx-target="#datarow-{{.ID}}"
hx-swap="outerHTML"
class="inline-flex items-center h-8 px-4 m-2 text-sm text-blue-100 transition-colors duration-150 bg-blue-700 rounded-lg focus:shadow-outline hover:bg-blue-800"
>
Edit
</button>
</tr> </tr>

View File

@@ -6,7 +6,7 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<script src="https://unpkg.com/htmx.org@1.9.2"></script> <script src="https://unpkg.com/htmx.org@1.9.2"></script>
<link href="/css/output.css" rel="stylesheet" /> <link href="/css/output.css" rel="stylesheet" />
<title>Go + HTMX + Tailwind</title> <title>Mikayla's Move Manager</title>
</head> </head>
<body> <body>
<main> <main>
@@ -34,7 +34,9 @@
</nav> </nav>
<br/> <br/>
<span class="text-xl">Items</span> <span class="text-xl">Items</span>
<div id="home-page-container"> <div id="home-page-container">
{{ template "entity-list.html" . }} {{ template "entity-list.html" . }}
</div> </div>

View File

@@ -5,8 +5,8 @@
type="text" type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
data-include-edit="{{.ID}}" data-include-edit="{{.ID}}"
name="company" name="Name"
value="{{.Company}}" value="{{.Name}}"
/> />
</td> </td>
<td class="whitespace-nowrap px-6 py-4"> <td class="whitespace-nowrap px-6 py-4">
@@ -14,8 +14,8 @@
type="text" type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
data-include-edit="{{.ID}}" data-include-edit="{{.ID}}"
name="contact" name="stage"
value="{{.Contact}}" value="{{.Stage}}"
/> />
</td> </td>
<td class="whitespace-nowrap px-6 py-4"> <td class="whitespace-nowrap px-6 py-4">
@@ -23,13 +23,31 @@
type="text" type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
data-include-edit="{{.ID}}" data-include-edit="{{.ID}}"
name="country" name="Category"
value="{{.Country}}" value="{{.Category}}"
/>
</td>
<td class="whitespace-nowrap px-6 py-4">
<input
type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
data-include-edit="{{.ID}}"
name="Description"
value="{{.Description}}"
/>
</td>
<td class="whitespace-nowrap px-6 py-4">
<input
type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
data-include-edit="{{.ID}}"
name="Notes"
value="{{.Notes}}"
/> />
</td> </td>
<td class="whitespace-nowrap px-1 py-1"> <td class="whitespace-nowrap px-1 py-1">
<a <a
hx-put="/company/{{.ID}}" hx-put="/items/{{.ID}}"
hx-target="#datarow-{{.ID}}" hx-target="#datarow-{{.ID}}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-indicator="#processing" hx-indicator="#processing"
@@ -41,7 +59,8 @@
</td> </td>
<td class="whitespace-nowrap px-1 py-1"> <td class="whitespace-nowrap px-1 py-1">
<a <a
hx-get="/company/{{.ID}}" hx-get="/items/{{.ID}}"
hx-params="edit=false"
hx-target="#datarow-{{.ID}}" hx-target="#datarow-{{.ID}}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-indicator="#processing" hx-indicator="#processing"

15
util/main.go Normal file
View File

@@ -0,0 +1,15 @@
package util
import (
"net/http"
"strconv"
"strings"
)
func GetIDFromPath(r *http.Request) (id int, err error) {
path := r.URL.Path
segments := strings.Split(path, "/")
last := segments[len(segments)-1]
id, err = strconv.Atoi(last)
return
}