initial commit
This commit is contained in:
parent
80cb26c286
commit
4fc9bb5c1f
|
@ -21,3 +21,5 @@
|
|||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# sqlite3 databases
|
||||
*.db
|
||||
|
|
|
@ -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) {
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (app *application) routes() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", app.home);
|
||||
return mux
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }}
|
|
@ -0,0 +1,5 @@
|
|||
{{ define "title" }}Home{{ end }}
|
||||
{{ define "main" }}
|
||||
<h2>Latest Guestbooks</h2>
|
||||
<p>There's nothing here yet</p>
|
||||
{{ end }}
|
|
@ -0,0 +1,5 @@
|
|||
{{ define "nav" }}
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
{{ end }}
|
Loading…
Reference in New Issue