Files
via/internal/examples/shakespeare/main.go
Ryan Hamamura a7ace9099f feat: replace log with rs/zerolog for structured logging
Switch from the standard library log package to rs/zerolog with
ConsoleWriter for colorful terminal output in dev mode and JSON
output in production. Users can now provide their own logger via
Options.Logger or set the level via Options.LogLevel.
2026-01-31 08:18:24 -10:00

179 lines
3.9 KiB
Go

package main
import (
"database/sql"
"fmt"
"log"
"time"
"github.com/ryanhamamura/via"
"github.com/ryanhamamura/via/h"
_ "github.com/mattn/go-sqlite3"
)
type DataSource interface {
Open()
Query(str string) (*sql.Rows, error)
Close() error
}
type ShakeDB struct {
db *sql.DB
findByTextStmt *sql.Stmt
}
func (shakeDB *ShakeDB) Prepare() {
db, err := sql.Open("sqlite3", "shake.db")
if err != nil {
log.Fatal(err)
}
stmt, err := db.Prepare(`select play,player,plays.text
from playsearch inner join plays on playsearch.playsrowid=plays.rowid where playsearch.text match ?
order by plays.play, plays.player limit 200;`)
if err != nil {
log.Fatal(err)
}
shakeDB.db = db
shakeDB.findByTextStmt = stmt
}
func (shakeDB *ShakeDB) Query(str string) (*sql.Rows, error) {
return shakeDB.findByTextStmt.Query(str)
}
func (shakeDB *ShakeDB) Close() {
if shakeDB.db != nil {
shakeDB.db.Close()
shakeDB.db = nil
}
}
func main() {
v := via.New()
v.Config(via.Options{
DevMode: true,
DocumentTitle: "Search",
LogLevel: via.LogLevelWarn,
})
v.AppendToHead(
h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")),
h.StyleEl(h.Raw(".no-wrap { white-space: nowrap; }")),
)
shakeDB := &ShakeDB{}
shakeDB.Prepare()
defer shakeDB.Close()
v.Page("/", func(c *via.Context) {
query := c.Signal("whether tis")
var rowsTable H
runQuery := func() {
qry := query.String()
start := time.Now()
rows, error := shakeDB.Query(qry)
fmt.Println("query ", qry, "took", time.Since(start))
if error != nil {
rowsTable = h.Div(h.Text("Error: " + error.Error()))
} else {
table, err := RenderTable(rows, []string{"no-wrap", "no-wrap", ""})
if err != nil {
rowsTable = h.Div(h.Text("Error: " + err.Error()))
} else {
rowsTable = table
}
}
}
runQueryAction := c.Action(func() {
runQuery()
c.Sync()
})
runQuery()
c.View(func() h.H {
return h.Div(
h.H2(h.Text("Search")), h.FieldSet(
h.Attr("role", "group"),
h.Input(
h.Type("text"),
query.Bind(),
h.Attr("autofocus"),
runQueryAction.OnKeyDown("Enter"),
),
h.Button(h.Text("Search"), runQueryAction.OnClick())),
rowsTable,
)
})
})
v.Start()
}
type H = h.H
func valueToString(v any) string {
if v == nil {
return ""
}
if b, ok := v.([]byte); ok {
return string(b)
}
return fmt.Sprint(v)
}
// RenderTable takes sql.Rows and an array of CSS class names for each column.
// Returns a complete HTML table as a gomponent.
func RenderTable(rows *sql.Rows, columnClasses []string) (H, error) {
cols, err := rows.Columns()
if err != nil {
return nil, err
}
headerCells := make([]h.H, len(cols))
for i, col := range cols {
headerCells[i] = h.Th(h.Attr("scope", "col"), h.Text(col))
}
thead := h.THead(h.Tr(headerCells...))
var bodyRows []h.H
for rows.Next() {
values := make([]any, len(cols))
scanArgs := make([]any, len(cols))
for i := range values {
scanArgs[i] = &values[i]
}
if err := rows.Scan(scanArgs...); err != nil {
return nil, err
}
cells := make([]h.H, len(values))
if len(values) > 0 {
var thAttrs []h.H
thAttrs = append(thAttrs, h.Attr("scope", "row"))
if len(columnClasses) > 0 && columnClasses[0] != "" {
thAttrs = append(thAttrs, h.Class(columnClasses[0]))
}
thAttrs = append(thAttrs, h.Text(valueToString(values[0])))
cells[0] = h.Th(thAttrs...)
for i := 1; i < len(values); i++ {
var tdAttrs []h.H
if i < len(columnClasses) && columnClasses[i] != "" {
tdAttrs = append(tdAttrs, h.Class(columnClasses[i]))
}
tdAttrs = append(tdAttrs, h.Text(valueToString(values[i])))
cells[i] = h.Td(tdAttrs...)
}
}
bodyRows = append(bodyRows, h.Tr(cells...))
}
if err := rows.Err(); err != nil {
return nil, err
}
tbody := h.TBody(bodyRows...)
return h.Table(thead, tbody), nil
}