initial commit

This commit is contained in:
yequari 2024-10-15 21:34:57 -07:00
parent 80cb26c286
commit 4fc9bb5c1f
15 changed files with 457 additions and 0 deletions

2
.gitignore vendored
View File

@ -21,3 +21,5 @@
# Go workspace file # Go workspace file
go.work go.work
# sqlite3 databases
*.db

30
cmd/web/handlers.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"net/http"
)
func (app *application) home(w http.ResponseWriter, r *http.Request) {
app.render(w, r, http.StatusOK, "home.tmpl.html", templateData{})
}
func getUserRegister(w http.ResponseWriter, r *http.Request) {
}
func postUserRegister(w http.ResponseWriter, r *http.Request) {
}
func getUsersList(w http.ResponseWriter, r *http.Request) {
}
func getUser(w http.ResponseWriter, r *http.Request) {
}
func postGuestbooksCreate(w http.ResponseWriter, r* http.Request) {
}
func getGuestbooksList(w http.ResponseWriter, r *http.Request) {
}
func getGuestbook(w http.ResponseWriter, r *http.Request) {
}

35
cmd/web/helpers.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"net/http"
"fmt"
)
func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
var (
method = r.Method
uri = r.URL.RequestURI()
)
app.logger.Error(err.Error(), "method", method, "uri", uri)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (app *application) clientError(w http.ResponseWriter, status int) {
http.Error(w, http.StatusText(status), status)
}
func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
ts, ok := app.templateCache[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, r, err)
return
}
w.WriteHeader(status)
err := ts.ExecuteTemplate(w, "base", data)
if err != nil {
app.serverError(w, r, err)
}
}

67
cmd/web/main.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"database/sql"
"flag"
"log/slog"
"net/http"
"os"
"text/template"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
_ "modernc.org/sqlite"
)
type application struct {
logger *slog.Logger
templateCache map[string]*template.Template
guestbooks *models.GuestbookModel
users *models.UserModel
guestbookComments *models.GuestbookCommentModel
}
func main() {
addr := flag.String("addr", ":3000", "HTTP network address")
dsn := flag.String("dsn", "guestbook.db", "data source name")
flag.Parse()
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
db, err := openDB(*dsn)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
defer db.Close()
templateCache, err := newTemplateCache()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
app := &application{
templateCache: templateCache,
logger: logger,
guestbooks: &models.GuestbookModel{DB: db},
users: &models.UserModel{DB: db},
guestbookComments: &models.GuestbookCommentModel{DB: db},
}
logger.Info("Starting server on %s", slog.Any("addr", ":4000"))
err = http.ListenAndServe(*addr, app.routes());
logger.Error(err.Error())
os.Exit(1)
}
func openDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}

12
cmd/web/routes.go Normal file
View File

@ -0,0 +1,12 @@
package main
import (
"net/http"
)
func (app *application) routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/", app.home);
return mux
}

51
cmd/web/templates.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"net/http"
"path/filepath"
"text/template"
"time"
)
type templateData struct {
CurrentYear int
}
func humanDate(t time.Time) string {
return t.Format("02 Jan 2006 at 15:04")
}
var functions = template.FuncMap {
"humanDate": humanDate,
}
func newTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
pages, err := filepath.Glob("./ui/html/pages/*.tmpl.html")
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl.html")
if err != nil {
return nil, err
}
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl.html")
if err != nil {
return nil, err
}
ts, err = ts.ParseFiles(page)
if err != nil {
return nil, err
}
cache[name] = ts
}
return cache, nil
}
func (app *application) newTemplateData(r *http.Request) *templateData {
return &templateData {
CurrentYear: time.Now().Year(),
}
}

View File

@ -0,0 +1,38 @@
CREATE TABLE users (
Id blob(16) primary key,
Username varchar(32) NOT NULL,
Email varchar(256) NOT NULL,
IsDeleted boolean NOT NULL DEFAULT FALSE
) WITHOUT ROWID;
CREATE TABLE guestbooks (
Id blob(16) primary key,
SiteUrl varchar(512) NOT NULL,
UserId blob(16) NOT NULL,
IsDeleted boolean NOT NULL DEFAULT FALSE,
IsActive boolean NOT NULL DEFAULT TRUE,
FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE guestbook_comments (
Id blob(16) primary key,
GuestbookId blob(16) NOT NULL,
ParentId blob(16),
AuthorName varchar(256) NOT NULL,
AuthorEmail varchar(256) NOT NULL,
AuthorSite varchar(256),
CommentText text NOT NULL,
PageUrl varchar(256),
IsPublished boolean NOT NULL DEFAULT TRUE,
IsDeleted boolean NOT NULL DEFAULT FALSE,
FOREIGN KEY (GuestbookId)
REFERENCES guestbooks(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (ParentId)
REFERENCES guestbook_comments(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module git.32bit.cafe/32bitcafe/guestbook
go 1.23.1
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.22.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.33.1 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

29
go.sum Normal file
View File

@ -0,0 +1,29 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@ -0,0 +1,47 @@
package models
import (
"database/sql"
"github.com/google/uuid"
)
type Guestbook struct {
ID uuid.UUID
SiteUrl string
UserId uuid.UUID
IsDeleted bool
IsActive bool
}
type GuestbookModel struct {
DB *sql.DB
}
func (m *GuestbookModel) Insert(siteUrl string, userId uuid.UUID) (uuid.UUID, error) {
id := uuid.New()
stmt := `INSERT INTO guestbooks (Id, SiteUrl, UserId, IsDeleted, IsActive)
VALUES(?, ?, FALSE, TRUE)`
_, err := m.DB.Exec(stmt, id, siteUrl, userId)
if err != nil {
return uuid.UUID{}, err
}
return id, nil
}
func (m *GuestbookModel) Get(id uuid.UUID) (Guestbook, error) {
stmt := `SELECT Id, SiteUrl, UserId, IsDeleted, IsActive FROM guestbooks
WHERE id = ?`
row := m.DB.QueryRow(stmt, id)
var g Guestbook
err := row.Scan(&g.ID, &g.SiteUrl, &g.UserId, &g.IsDeleted, &g.IsActive)
if err != nil {
return Guestbook{}, err
}
return g, nil
}
func (m *GuestbookModel) GetAll(userId uuid.UUID) ([]Guestbook, error) {
return []Guestbook{}, nil
}

View File

@ -0,0 +1,53 @@
package models
import (
"database/sql"
"github.com/google/uuid"
)
type GuestbookComment struct {
ID uuid.UUID
GuestbookId uuid.UUID
ParentId uuid.UUID
AuthorName string
AuthorEmail string
AuthorSite string
CommentText string
PageUrl string
IsPublished bool
IsDeleted bool
}
type GuestbookCommentModel struct {
DB *sql.DB
}
func (m *GuestbookCommentModel) Insert(guestbookId, parentId uuid.UUID, authorName,
authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (uuid.UUID, error) {
id := uuid.New()
stmt := `INSERT INTO guestbook_comments (Id, GuestbookId, ParentId, AuthorName,
AuthorEmail, AuthorSite, CommentText, PageUrl, IsPublished, IsDeleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)`
_, err := m.DB.Exec(stmt, id, guestbookId, parentId, authorName, authorEmail,
authorSite, commentText, pageUrl, isPublished)
if err != nil {
return uuid.UUID{}, err
}
return id, nil
}
func (m *GuestbookCommentModel) Get(id uuid.UUID) (GuestbookComment, error) {
stmt := `SELECT Id, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, IsPublished, IsDeleted FROM guestbook_comments WHERE id = ?`
row := m.DB.QueryRow(stmt, id)
var c GuestbookComment
err := row.Scan(&c.ID, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite, &c.CommentText, &c.PageUrl, &c.IsPublished, &c.IsDeleted)
if err != nil {
return GuestbookComment{}, err
}
return c, nil
}
func (m *GuestbookCommentModel) GetAll(guestbookId *uuid.UUID) {
}

40
internal/models/user.go Normal file
View File

@ -0,0 +1,40 @@
package models
import (
"database/sql"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID
Username string
Email string
IsDeleted bool
}
type UserModel struct {
DB *sql.DB
}
func (m *UserModel) Insert(username string, email string) (uuid.UUID, error) {
id := uuid.New()
stmt := `INSERT INTO users (Id, Username, Email, IsDeleted)
VALUES (?, ?, ?, FALSE)`
_, err := m.DB.Exec(stmt, id, username, email)
if err != nil {
return uuid.UUID{}, err
}
return id, nil
}
func (m *UserModel) Get(id uuid.UUID) (User, error) {
stmt := `SELECT Id, Username, Email, IsDeleted FROM users WHERE id = ?`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.Username, &u.Email, &u.IsDeleted)
if err != nil {
return User{}, err
}
return u, nil
}

23
ui/html/base.tmpl.html Normal file
View File

@ -0,0 +1,23 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ template "title" }} - Guestbook</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<header>
<h1><a href="/">Guestbook</a></h1>
</header>
{{ template "nav" . }}
<main>
{{ template "main" . }}
</main>
<footer>
<p>A 32-bit Cafe Project</p>
</footer>
</body>
</html>
{{ end }}

View File

@ -0,0 +1,5 @@
{{ define "title" }}Home{{ end }}
{{ define "main" }}
<h2>Latest Guestbooks</h2>
<p>There's nothing here yet</p>
{{ end }}

View File

@ -0,0 +1,5 @@
{{ define "nav" }}
<nav>
<a href="/">Home</a>
</nav>
{{ end }}