Skip to content

Commit

Permalink
Adding HTMX for a bit of excitement
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Balogh <[email protected]>
  • Loading branch information
javaducky committed May 22, 2024
1 parent 0570d9e commit a3e3552
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 21 deletions.
5 changes: 5 additions & 0 deletions internal/app/place.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ func (ctx *Context) GetPlaces() ([]*model.Place, error) {
return ctx.Database.GetPlaces()
}

// SearchPlaces returns available places.
func (ctx *Context) SearchPlaces(s string) ([]*model.Place, error) {
return ctx.Database.SearchPlaces(s)
}

// GetPlaceByID returns the place specified by the provided identifier.
func (ctx *Context) GetPlaceByID(id uint) (*model.Place, error) {
place, err := ctx.Database.GetPlaceByID(id)
Expand Down
10 changes: 10 additions & 0 deletions internal/db/place.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package db

import (
"github.com/pkg/errors"
"strings"

"github.com/weesvc/weesvc-gorilla/internal/model"
)
Expand Down Expand Up @@ -32,3 +33,12 @@ func (db *Database) UpdatePlace(place *model.Place) error {
func (db *Database) DeletePlaceByID(id uint) error {
return errors.Wrap(db.Delete(&model.Place{}, id).Error, "unable to delete place")
}

func (db *Database) SearchPlaces(s string) ([]*model.Place, error) {
var places []*model.Place
s = strings.ToLower(s)
return places, errors.Wrap(
db.Where("LOWER(name) LIKE ? OR LOWER(description) LIKE ?",
"%"+s+"%", "%"+s+"%").Limit(10).Find(&places).Error,
"unable to find places")
}
1 change: 1 addition & 0 deletions internal/server/assets/htmx.min.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions internal/server/assets/wee-stylee.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tr.htmx-swapping td {
opacity: 0;
transition: opacity 1s ease-out;
}
106 changes: 102 additions & 4 deletions internal/server/handlers/places.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,123 @@ package handlers

import (
"github.com/a-h/templ"
"github.com/gorilla/mux"
"github.com/weesvc/weesvc-gorilla/internal/app"
"github.com/weesvc/weesvc-gorilla/internal/server/views"
"net/http"
"strconv"
)

type PlacesHandler struct {
Application *app.App
Service *app.Context
}

func NewPlacesHandler(app *app.App) *PlacesHandler {
return &PlacesHandler{app}
return &PlacesHandler{Application: app, Service: app.NewContext()}
}

func (h *PlacesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
svc := h.Application.NewContext()
places, err := svc.GetPlaces()
func (h *PlacesHandler) GetPlaces(w http.ResponseWriter, r *http.Request) {
places, err := h.Service.GetPlaces()
if err != nil {
panic(err)
}

templ.Handler(views.Layout(views.Places(places))).ServeHTTP(w, r)
}

func (h *PlacesHandler) SearchPlaces(w http.ResponseWriter, r *http.Request) {
search := r.FormValue("search")
places, err := h.Service.SearchPlaces(search)
if err != nil {
panic(err)
}

templ.Handler(views.PlaceRows(places)).ServeHTTP(w, r)
}

func (h *PlacesHandler) GetPlaceByID(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
place, err := h.Service.GetPlaceByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
templ.Handler(views.Layout(views.PlaceDetailsPage(place))).ServeHTTP(w, r)
}

func (h *PlacesHandler) GetPlaceDetails(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
place, err := h.Service.GetPlaceByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
templ.Handler(views.PlaceDetails(place)).ServeHTTP(w, r)
}

func (h *PlacesHandler) GetPlaceEditor(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
place, err := h.Service.GetPlaceByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
templ.Handler(views.PlaceEditor(place)).ServeHTTP(w, r)
}

func (h *PlacesHandler) UpdatePlaceByID(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
place, err := h.Service.GetPlaceByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
}
place.Name = r.FormValue("name")
place.Description = r.FormValue("description")
place.Latitude, err = strconv.ParseFloat(r.FormValue("latitude"), 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
place.Longitude, err = strconv.ParseFloat(r.FormValue("longitude"), 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
err = h.Service.UpdatePlace(place)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
templ.Handler(views.PlaceDetails(place)).ServeHTTP(w, r)
}

func (h *PlacesHandler) DeletePlaceByID(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
err = h.Service.DeletePlaceByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}

func getIDFromRequest(r *http.Request) (uint, error) {
vars := mux.Vars(r)
id := vars["id"]

intID, err := strconv.ParseInt(id, 10, 0)
if err != nil {
return 0, err
}

return uint(intID), nil
}
12 changes: 11 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ func StartServer(config *config.Config) {
restapi.Init(router.PathPrefix("/api").Subrouter())

router.Handle("/", handlers.NewWelcomeHandler()).Methods("GET")
router.Handle("/places", handlers.NewPlacesHandler(svc)).Methods("GET")

ph := handlers.NewPlacesHandler(svc)
router.HandleFunc("/places", ph.GetPlaces).Methods("GET")
//router.HandleFunc("/places", ph.CreatePlace).Methods("POST")
router.HandleFunc("/places/{id:[0-9]+}", ph.GetPlaceByID).Methods("GET")
router.HandleFunc("/places/{id:[0-9]+}/edit", ph.GetPlaceEditor).Methods("GET")
router.HandleFunc("/places/{id:[0-9]+}/cancel", ph.GetPlaceDetails).Methods("GET")
router.HandleFunc("/places/{id:[0-9]+}", ph.UpdatePlaceByID).Methods("PUT")
router.HandleFunc("/places/{id:[0-9]+}", ph.DeletePlaceByID).Methods("DELETE")
router.HandleFunc("/places/search", ph.SearchPlaces).Methods("POST")

router.PathPrefix("/assets/").Handler(handlers.NewStaticHandler(wwwroot, "assets"))

router.NotFoundHandler = handlers.NewNotFoundHandler()
Expand Down
5 changes: 2 additions & 3 deletions internal/server/views/layout.templ
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ templ Layout(content templ.Component) {
<title>WeeSVC - A Tiny Server</title>
<link href="/assets/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="/assets/dist/js/bootstrap.min.js"></script>
<script src="/assets/htmx.min.js"></script>
<link href="/assets/wee-stylee.css" rel="stylesheet" />
</head>
<body>
@navbar()
Expand All @@ -33,9 +35,6 @@ templ navbar() {
<li class="nav-item"><a href="/" class="nav-link">Home</a></li>
<li class="nav-item"><a href="/places" class="nav-link">Places</a></li>
</ul>
<form class="ui-widget" role="search">
<input id="search" class="form-control ui-autocomplete-input" autocomplete="off" type="search" placeholder="Search" aria-label="Search" />
</form>
</div>
</div>
</nav>
Expand Down
47 changes: 47 additions & 0 deletions internal/server/views/place.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package views

import "fmt"
import "github.com/weesvc/weesvc-gorilla/internal/model"

templ PlaceDetailsPage(place *model.Place) {
<h2 class="display-4">{ place.Name }</h2>
<div class="card-text">
@PlaceDetails(place)
</div>
}


templ PlaceDetails(place *model.Place) {
<div hx-target="this" hx-swap="outerHTML">
<div><label>Name</label>: { place.Name }</div>
<div><label>Description</label>: { place.Description }</div>
<div><label>Latitude</label>: { fmt.Sprintf("%f", place.Latitude) }</div>
<div><label>Latitude</label>: { fmt.Sprintf("%f", place.Longitude) }</div>
<button hx-get={ fmt.Sprintf("/places/%d/edit", place.ID) } class="btn btn-primary">
Click to Edit
</button>
</div>
}

templ PlaceEditor(place *model.Place) {
<form hx-put={ fmt.Sprintf("/places/%d", place.ID) } hx-target="this" hx-swap="outerHTML">
<div>
<label>Name</label>
<input type="text" name="name" value={ place.Name }>
</div>
<div class="form-group">
<label>Description</label>
<input type="text" name="description" value={ place.Description }>
</div>
<div class="form-group">
<label>Latitude</label>
<input type="text" name="latitude" value={ fmt.Sprintf("%f", place.Latitude) }>
</div>
<div class="form-group">
<label>Latitude</label>
<input type="text" name="longitude" value={ fmt.Sprintf("%f", place.Longitude) }>
</div>
<button class="btn btn-primary">Submit</button>
<button class="btn btn-primary" hx-get={ fmt.Sprintf("/places/%d/cancel", place.ID) } hx-swap="outerHTML">Cancel</button>
</form>
}
51 changes: 38 additions & 13 deletions internal/server/views/places.templ
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ templ Places(places []*model.Place) {
<h2 class="display-4">Places</h2>
<div class="card-text">
<div>
<h3>
Search Contacts
<span class="htmx-indicator">
<img src="/img/bars.svg"/> Searching...
</span>
</h3>
<input class="form-control" type="search"
name="search" placeholder="Begin Typing To Search Places..."
hx-post="/places/search"
hx-trigger="input changed delay:500ms, search"
hx-target="#search-results"
hx-indicator=".htmx-indicator" />
<table id="datatable" class="table table-striped table-hover mb-0">
<thead class="sticky-top table-secondary">
<tr>
Expand All @@ -18,21 +30,34 @@ templ Places(places []*model.Place) {
<th scope="col"></th>
</tr>
</thead>
<tbody>
for _, place := range places {
<tr>
<th scope="row">{ fmt.Sprintf("%d", place.ID) }</th>
<td>{ place.Name }</td>
<td>{ place.Description }</td>
<td>{ fmt.Sprintf("%f", place.Latitude) }</td>
<td>{ fmt.Sprintf("%f", place.Longitude) }</td>
<td>
<a class="btn btn-dark" href={ templ.URL(fmt.Sprintf("https://google.com/search?q=%f,%f", place.Latitude, place.Longitude)) } target="_blank">Visit</a>
</td>
</tr>
}
<tbody id="search-results" hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML swap:1s">
@PlaceRows(places)
</tbody>
</table>
</div>
</div>
}

templ PlaceRows(places []*model.Place) {
for _, place := range places {
<tr>
<th scope="row">{ fmt.Sprintf("%d", place.ID) }</th>
<td>
<a class="icon-link icon-link-hover" href={ templ.URL(fmt.Sprintf("/places/%d", place.ID)) }>
{ place.Name }
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-gear" viewBox="0 0 16 16">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"/>
</svg>
</a>
</td>
<td>{ place.Description }</td>
<td>{ fmt.Sprintf("%f", place.Latitude) }</td>
<td>{ fmt.Sprintf("%f", place.Longitude) }</td>
<td>
<a class="btn btn-dark" href={ templ.URL(fmt.Sprintf("https://google.com/search?q=%f,%f", place.Latitude, place.Longitude)) } target="_blank">Visit</a>
<button class="btn btn-danger" hx-delete={ fmt.Sprintf("/places/%d", place.ID) }>Delete</button>
</td>
</tr>
}
}

0 comments on commit a3e3552

Please sign in to comment.