diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go new file mode 100644 index 0000000..875968c --- /dev/null +++ b/cmd/web/handlers.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "errors" + "net/http" + "text/template" + + "git.32bit.cafe/yequari/webring/internal/models" +) + +func (app *application) home(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + files := []string{ + "./ui/html/base.tmpl.html", + "./ui/html/pages/home.tmpl.html", + } + + ts, err := template.ParseFiles(files...) + if err != nil { + app.serverError(w, err) + return + } + + err = ts.ExecuteTemplate(w, "base", nil) + if err != nil { + app.serverError(w, err) + } +} + +func (app *application) webmasterView(w http.ResponseWriter, r *http.Request) { + id := models.WebmasterId(r.URL.Query().Get("id")) + + webmaster, err := app.webmasters.Get(id) + if err != nil { + if errors.Is(err, models.ErrNoRecord) { + app.notFound(w) + } else { + app.serverError(w, err) + } + return + } + + fmt.Fprintf(w, "%+v", webmaster) +} + +func (app *application) webmasterCreate(w http.ResponseWriter, r *http.Request) { +} + +func (app *application) siteEntryView(w http.ResponseWriter, r *http.Request) { + id := models.SiteId(r.URL.Query().Get("id")) + + siteEntry, err := app.siteEntries.Get(id) + if err != nil { + if errors.Is(err, models.ErrNoRecord) { + app.notFound(w) + } else { + app.serverError(w, err) + } + return + } + + fmt.Fprintf(w, "%+v", siteEntry) +} + +func (app *application) siteEntryCreate(w http.ResponseWriter, r *http.Request) { +} + +func (app *application) nextSiteEntry(w http.ResponseWriter, r *http.Request) { +} + +func (app *application) prevSiteEntry(w http.ResponseWriter, r *http.Request) { +} + diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go new file mode 100644 index 0000000..4f76b13 --- /dev/null +++ b/cmd/web/helpers.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "net/http" + "runtime/debug" +) + +func (app *application) serverError(w http.ResponseWriter, err error) { + trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack()) + app.errorLog.Print(trace) + + 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) notFound(w http.ResponseWriter) { + app.clientError(w, http.StatusNotFound) +} diff --git a/cmd/web/main.go b/cmd/web/main.go new file mode 100644 index 0000000..2f8a994 --- /dev/null +++ b/cmd/web/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" + "database/sql" + + "git.32bit.cafe/yequari/webring/internal/models" + + _ "modernc.org/sqlite" +) + +type application struct { + errorLog *log.Logger + infoLog *log.Logger + siteEntries *models.SiteEntryModel + webmasters *models.WebmasterModel +} + +func main() { + addr := flag.String("addr", ":8000", "HTTP network address") + dsn := flag.String("dsn", "webring.db", "data source name") + flag.Parse() + + infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) + errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) + + db, err := openDB(*dsn) + if err != nil { + errorLog.Fatal(err) + } + defer db.Close() + + app := &application{ + errorLog: errorLog, + infoLog: infoLog, + siteEntries: &models.SiteEntryModel{DB: db}, + webmasters: &models.WebmasterModel{DB: db}, + } + + srv := &http.Server{ + Addr: *addr, + ErrorLog: errorLog, + Handler: app.routes(), + } + + infoLog.Printf("Starting server on %s", *addr) + err = srv.ListenAndServe() + errorLog.Fatal(err) +} + +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 +} diff --git a/cmd/web/routes.go b/cmd/web/routes.go new file mode 100644 index 0000000..ce26a71 --- /dev/null +++ b/cmd/web/routes.go @@ -0,0 +1,17 @@ +package main + +import "net/http" + +func (app *application) routes() *http.ServeMux { + mux := http.NewServeMux() + + fileServer := http.FileServer(http.Dir("./ui/static")) + mux.Handle("/static/", http.StripPrefix("/static", fileServer)) + + // TODO: add more handlers + mux.HandleFunc("/", app.home) + mux.HandleFunc("/webmasters/view", app.webmasterView) + mux.HandleFunc("/sites/view", app.siteEntryView) + + return mux +} diff --git a/internal/models/errors.go b/internal/models/errors.go new file mode 100644 index 0000000..c7845a7 --- /dev/null +++ b/internal/models/errors.go @@ -0,0 +1,5 @@ +package models + +import "errors" + +var ErrNoRecord = errors.New("models: no matching record found") diff --git a/internal/models/siteentry.go b/internal/models/siteentry.go new file mode 100644 index 0000000..d853c72 --- /dev/null +++ b/internal/models/siteentry.go @@ -0,0 +1,83 @@ +package models + +import ( + "database/sql" + "time" + "errors" + + "github.com/google/uuid" +) + +type SiteId string + +type SiteEntry struct { + Id SiteId + Name string + Webmaster WebmasterId + Url string + DateAdded time.Time + Next SiteId + Prev SiteId +} + +type SiteEntryModel struct { + DB *sql.DB +} + +// Commit a SiteEntry to the database +func (m *SiteEntryModel) Insert(name string, url string, webmaster *Webmaster) (SiteId, error) { + stmt := `INSERT INTO siteentries (id, name, url, webmaster, dateAdded) + VALUES(?,?,?,?,datetime("now"))` + + id := uuid.NewString() + _, err := m.DB.Exec(stmt, id, name, url, webmaster.Id) + if err != nil { + return "", nil + } + + return SiteId(id), nil +} + +// Retrieve a SiteEntry from the database by id +func (m *SiteEntryModel) Get(id SiteId) (*SiteEntry, error) { + stmt := `SELECT id, name, url, webmaster, dateAdded FROM siteentries + WHERE id = ?` + + row := m.DB.QueryRow(stmt, id) + s := &SiteEntry{} + + + var timeStr string + err := row.Scan(&s.Id, &s.Name, &s.Url, &s.Webmaster, &timeStr) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNoRecord + } else { + return nil, err + } + } + // time is stored as a string in sqlite, so we need to parse it + // probably should be something taken care of in the driver + dateAdded, err := time.Parse(time.DateTime, timeStr) + if err != nil { + return nil, err + } + s.DateAdded = dateAdded + + return s, nil +} + +// Retrieve a SiteEntry from the database by url +func (m *SiteEntryModel) GetByUrl(url string) (*SiteEntry, error) { + return nil, nil +} + +// Update existing SiteEntry with the values of passed entry +func (m *SiteEntryModel) Update(entry *SiteEntry) error { + return nil +} + +// Delete SiteEntry from database +func (m *SiteEntryModel) Delete(entry *SiteEntry) error { + return nil +} diff --git a/internal/models/webmaster.go b/internal/models/webmaster.go new file mode 100644 index 0000000..c83fc4f --- /dev/null +++ b/internal/models/webmaster.go @@ -0,0 +1,64 @@ +package models + +import ( + "database/sql" + "errors" + + "github.com/google/uuid" +) + +type WebmasterId string + +type Webmaster struct { + Id WebmasterId + Name string + Email string +} + +type WebmasterModel struct { + DB *sql.DB +} + +// Commit a SiteWebmaster to the database +func (m *WebmasterModel) Insert(name string, email string) (WebmasterId, error) { + stmt := `INSERT INTO webmasters (id, name, email) + VALUES(?, ?, ?)` + + id := uuid.NewString() + _, err := m.DB.Exec(stmt, id, name, email) + if err != nil { + return "", err + } + + return WebmasterId(id), nil +} + +// Retrieve a SiteWebmasterModel from the database by id +func (m *WebmasterModel) Get(id WebmasterId) (*Webmaster, error) { + stmt := `SELECT id, name, email FROM webmasters + WHERE id = ?` + + result := m.DB.QueryRow(stmt, id) + w := &Webmaster{} + + err := result.Scan(&w.Id, &w.Name, &w.Email) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNoRecord + } else { + return nil, err + } + } + + return w, nil +} + +// Update a SiteWebmasterModel in the database with the values of the passed webmaster +func (m *WebmasterModel) Update(webmaster *Webmaster) error { + return nil +} + +// Delete a SiteWebmasterModel from the database +func (m *WebmasterModel) Delete(webmaster *Webmaster) error { + return nil +} diff --git a/main.go b/main.go deleted file mode 100644 index ebde166..0000000 --- a/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "fmt" - "git.32bit.cafe/yequari/webring/webring" -) - -func main() { - site := webring.NewSiteEntry("example", "me", "example.com") - fmt.Println(site) -} diff --git a/ui/html/base.tmpl.html b/ui/html/base.tmpl.html new file mode 100644 index 0000000..8f42e53 --- /dev/null +++ b/ui/html/base.tmpl.html @@ -0,0 +1,17 @@ +{{define "base"}} + + + + + {{template "title" .}} - 32-Bit Cafe + + +
+

Webring

+
+
+ {{template "main" .}} +
+ + +{{end}} diff --git a/ui/html/pages/home.tmpl.html b/ui/html/pages/home.tmpl.html new file mode 100644 index 0000000..48c09ef --- /dev/null +++ b/ui/html/pages/home.tmpl.html @@ -0,0 +1,6 @@ +{{define "title"}}Home{{end}} + +{{define "main"}} +

Latest Websites

+

There's nothing to see here yet!

+{{end}} diff --git a/webring/webring.go b/webring/webring.go deleted file mode 100644 index 5f35a34..0000000 --- a/webring/webring.go +++ /dev/null @@ -1,111 +0,0 @@ -package webring - -import ( - "database/sql" - "time" - - "github.com/google/uuid" -) - -type SiteId string - -type SiteEntry struct { - Id SiteId - Name string - Webmaster *SiteWebmaster - Url string - DateAdded time.Time - Next SiteId - Prev SiteId -} - - -func NewSiteEntry(siteName, webmasterEmail, siteUrl string) SiteEntry { - // next site is the first one in the ring - // previous site is the last one inserted - // retrieve webmaster from database or create if it doesn't exist - return SiteEntry{ - Id: SiteId(uuid.NewString()), - Name: siteName, - Url: siteUrl, - DateAdded: time.Now(), - } -} - -type WebmasterId string - -type SiteWebmaster struct { - Id WebmasterId - Name string - Email string -} - -func NewSiteWebmaster(name, email string) SiteWebmaster { - return SiteWebmaster{ Id: WebmasterId(uuid.NewString()), - Name: name, - Email: email, - } -} - -type Webring struct { - Db *sql.DB - Length int - first SiteId - last SiteId -} - -// Retrieve the first website added to the webring -func (webring *Webring) retrieveFirstSite() (*SiteEntry, error) { - return nil, nil -} - -// Retrieve the latest website added to the webring -func (webring *Webring) retrieveLastSite() (*SiteEntry, error) { - return nil, nil -} - -// Commit a SiteEntry to the database -func (webring *Webring) CreateSiteEntry(entry *SiteEntry) error { - return nil -} - -// Retrieve a SiteEntry from the database by id -func (webring *Webring) RetrieveSiteEntry(id SiteId) (*SiteEntry, error) { - return nil, nil -} - -// Retrieve a SiteEntry from the database by url -func (webring *Webring) RetriveSiteEntryByUrl(url string) (*SiteEntry, error) { - return nil, nil -} - -// Update existing SiteEntry with the values of passed entry -func (webring *Webring) UpdateSiteEntry(entry *SiteEntry) error { - return nil -} - -// Delete SiteEntry from database -func (webring *Webring) DeleteSiteEntry(entry *SiteEntry) error { - return nil -} - -// Commit a SiteWebmaster to the database -func (webring *Webring) CreateSiteWebmaster(webmaster *SiteWebmaster) error { - return nil -} - -// Retrieve a SiteWebmaster from the database by id -func (webring *Webring) RetrieveSiteWebmaster(id WebmasterId) (*SiteWebmaster, error) { - return nil, nil -} - -// Update a SiteWebmaster in the database with the values of the passed webmaster -func (webring *Webring) UpdateSiteWebmaster(webmaster *SiteWebmaster) error { - return nil -} - -// Delete a SiteWebmaster from the database -func (webring *Webring) DeleteSiteWebmaster(webmaster *SiteWebmaster) error { - return nil -} -