summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth@simponic.xyz>2024-08-17 18:29:33 -0400
committersimponic <simponic@hatecomputers.club>2024-08-17 18:29:33 -0400
commitb1775c4408bb00803eba321aa66ab92d6ba45580 (patch)
tree90179edff8951b06abb91495ce21c8b1841d2d82
parent0b8883c236a06a14e5e6958ed47f89729b0e41aa (diff)
downloadhatecomputers.club-b1775c4408bb00803eba321aa66ab92d6ba45580.tar.gz
hatecomputers.club-b1775c4408bb00803eba321aa66ab92d6ba45580.zip
kennel (#13)
Reviewed-on: https://git.hatecomputers.club/hatecomputers/hatecomputers.club/pulls/13 Co-authored-by: Elizabeth Hunt <elizabeth@simponic.xyz> Co-committed-by: Elizabeth Hunt <elizabeth@simponic.xyz>
-rw-r--r--adapters/files/files_adapter.go1
-rw-r--r--adapters/files/filesystem/filesystem.go5
-rw-r--r--api/kennel/kennel.go238
-rw-r--r--api/serve.go35
-rw-r--r--database/kennel.go159
-rw-r--r--database/migrate.go21
-rw-r--r--static/css/colors.css4
-rw-r--r--static/css/table.css2
-rw-r--r--static/img/cat_spritesheets/default.gifbin0 -> 6512 bytes
-rw-r--r--templates/base.html2
-rw-r--r--templates/dns.html2
-rw-r--r--templates/kennel_cats.html66
12 files changed, 530 insertions, 5 deletions
diff --git a/adapters/files/files_adapter.go b/adapters/files/files_adapter.go
index bf3ea5f..44853e1 100644
--- a/adapters/files/files_adapter.go
+++ b/adapters/files/files_adapter.go
@@ -4,5 +4,6 @@ import "io"
type FilesAdapter interface {
CreateFile(path string, content io.Reader) (string, error)
+ FileExists(path string) bool
DeleteFile(path string) error
}
diff --git a/adapters/files/filesystem/filesystem.go b/adapters/files/filesystem/filesystem.go
index 726a588..e7e671f 100644
--- a/adapters/files/filesystem/filesystem.go
+++ b/adapters/files/filesystem/filesystem.go
@@ -35,3 +35,8 @@ func (f *FilesystemAdapter) CreateFile(path string, content io.Reader) (string,
func (f *FilesystemAdapter) DeleteFile(path string) error {
return os.Remove(f.BasePath + path)
}
+
+func (f *FilesystemAdapter) FileExists(path string) bool {
+ _, err := os.Stat(f.BasePath + path)
+ return err == nil
+}
diff --git a/api/kennel/kennel.go b/api/kennel/kennel.go
new file mode 100644
index 0000000..a68388d
--- /dev/null
+++ b/api/kennel/kennel.go
@@ -0,0 +1,238 @@
+package kennel
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "strings"
+
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/files"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/types"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
+)
+
+const MaxCatSize = 1024 * 100 // 60KB
+const CatsPath = "cats/"
+const CatsPrefix = "/uploads/cats/"
+const DefaultCatSpritesheet = "/static/img/cat_spritesheets/default.gif"
+const MaxUserCats = 15
+
+func ListUserCatsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ userID := context.User.ID
+
+ cats, err := database.GetUserKennelCats(context.DBConn, userID)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ (*context.TemplateData)["Cats"] = cats
+ return success(context, req, resp)
+ }
+}
+
+func CreateCatContinuation(fileAdapter files.FilesAdapter, maxUserCats int, maxCatSize int, catsPath string, catsPrefix string, defaultCatSpritesheet string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ formErrors := types.BannerMessages{
+ Messages: []string{},
+ }
+
+ numCats, err := database.CountUserKennelCats(context.DBConn, context.User.ID)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+ if numCats >= maxUserCats {
+ formErrors.Messages = append(formErrors.Messages, "max cats reached for user")
+ }
+
+ err = req.ParseMultipartForm(int64(maxCatSize))
+ if err != nil {
+ formErrors.Messages = append(formErrors.Messages, "cat spritesheet too large")
+ }
+
+ catID := utils.RandomId()
+ spritesheetPath := catsPrefix + catID
+
+ if len(formErrors.Messages) == 0 {
+ file, _, err := req.FormFile("spritesheet")
+ if file != nil && err != nil {
+ formErrors.Messages = append(formErrors.Messages, "error uploading spritesheet")
+ } else if file != nil {
+ defer file.Close()
+ reader := http.MaxBytesReader(resp, file, int64(maxCatSize))
+ defer reader.Close()
+
+ _, err = fileAdapter.CreateFile(catsPath+catID, reader)
+ if err != nil {
+ log.Println(err)
+ formErrors.Messages = append(formErrors.Messages, "error saving spritesheet (is it too big?)")
+ }
+ } else if file == nil && err != nil {
+ spritesheetPath = defaultCatSpritesheet
+ }
+ }
+
+ link := req.FormValue("link")
+ description := req.FormValue("description")
+ name := req.FormValue("name")
+
+ cat := &database.KennelCat{
+ ID: catID,
+ UserID: context.User.ID,
+ Name: name,
+ Link: link,
+ Description: description,
+ Spritesheet: spritesheetPath,
+ }
+ formErrors.Messages = append(formErrors.Messages, validateCat(cat)...)
+ if len(formErrors.Messages) == 0 {
+ _, err := database.SaveKennelCat(context.DBConn, cat)
+ if err != nil {
+ log.Println(err)
+ formErrors.Messages = append(formErrors.Messages, "failed to save cat")
+ }
+ }
+
+ if len(formErrors.Messages) > 0 {
+ (*context.TemplateData)["Error"] = formErrors
+ (*context.TemplateData)["CatForm"] = cat
+ resp.WriteHeader(http.StatusBadRequest)
+
+ return failure(context, req, resp)
+ }
+
+ formSuccess := types.BannerMessages{
+ Messages: []string{"cat added."},
+ }
+ (*context.TemplateData)["Success"] = formSuccess
+ return success(context, req, resp)
+ }
+ }
+}
+
+func RemoveCatContinuation(fileAdapter files.FilesAdapter, catsPath string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ catID := req.FormValue("id")
+
+ cat, err := database.GetKennelCat(context.DBConn, catID)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+ if cat == nil || cat.UserID != context.User.ID {
+ resp.WriteHeader(http.StatusUnauthorized)
+ return failure(context, req, resp)
+ }
+
+ err = database.DeleteKennelCat(context.DBConn, catID)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ err = fileAdapter.DeleteFile(catsPath + catID)
+ if err != nil && fileAdapter.FileExists(catsPath+catID) {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ return success(context, req, resp)
+ }
+ }
+}
+
+func RingContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ order := req.URL.Query().Get("order")
+
+ if order == "random" {
+ kennelCat, err := database.GetRandomKennelCat(context.DBConn)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+ http.Redirect(resp, req, kennelCat.Link, http.StatusFound)
+ return success(context, req, resp)
+ }
+
+ id := req.URL.Query().Get("id")
+ if id == "" {
+ resp.WriteHeader(http.StatusBadRequest)
+ return failure(context, req, resp)
+ }
+ if order != "random" && order != "next" && order != "prev" {
+ kennelCat, err := database.GetKennelCat(context.DBConn, id)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusNotFound)
+ return failure(context, req, resp)
+ }
+ http.Redirect(resp, req, kennelCat.Link, http.StatusFound)
+ return success(context, req, resp)
+ }
+
+ nextCat, err := database.GetNextKennelCat(context.DBConn, id, order == "next")
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ http.Redirect(resp, req, nextCat.Link, http.StatusFound)
+ return success(context, req, resp)
+ }
+}
+
+func GetKennelContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+ return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
+ cats, err := database.GetKennel(context.DBConn)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+ json, err := json.Marshal(cats)
+ if err != nil {
+ log.Println(err)
+ resp.WriteHeader(http.StatusInternalServerError)
+ return failure(context, req, resp)
+ }
+
+ resp.Header().Set("Content-Type", "application/json")
+ resp.Write(json)
+ return success(context, req, resp)
+ }
+}
+
+func validateCat(cat *database.KennelCat) []string {
+ errors := []string{}
+
+ if cat.Name == "" {
+ errors = append(errors, "name is required")
+ }
+ if cat.Link == "" {
+ errors = append(errors, "link is required")
+ }
+ if !strings.HasPrefix(cat.Link, "http://") && !strings.HasPrefix(cat.Link, "https://") {
+ errors = append(errors, "link must be a valid URL")
+ }
+ if cat.Description == "" {
+ errors = append(errors, "description is required")
+ }
+ if len(cat.Description) > 100 {
+ errors = append(errors, "description must be less than 100 characters")
+ }
+
+ return errors
+}
diff --git a/api/serve.go b/api/serve.go
index ca8142b..e205ce5 100644
--- a/api/serve.go
+++ b/api/serve.go
@@ -13,6 +13,7 @@ import (
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/dns"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/guestbook"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/hcaptcha"
+ "git.hatecomputers.club/hatecomputers/hatecomputers.club/api/kennel"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/keys"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/profiles"
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api/template"
@@ -172,10 +173,38 @@ func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(hcaptcha.CaptchaVerificationContinuation, hcaptcha.CaptchaVerificationContinuation)(guestbook.SignGuestbookContinuation, FailurePassingContinuation)(guestbook.ListGuestbookContinuation, guestbook.ListGuestbookContinuation)(hcaptcha.CaptchaArgsContinuation, hcaptcha.CaptchaArgsContinuation)(template.TemplateContinuation("guestbook.html", true), template.TemplateContinuation("guestbook.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
- mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
+ mux.HandleFunc("GET /kennel/", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
- name := r.PathValue("name")
- LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(template.TemplateContinuation(name+".html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ LogRequestContinuation(requestContext, r, w)(kennel.GetKennelContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /kennel/cat", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(kennel.RingContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /kennel/cats", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, auth.GoLoginContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("POST /kennel/cats", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ createCatContinuation := kennel.CreateCatContinuation(uploadAdapter, kennel.MaxUserCats, kennel.MaxCatSize, kennel.CatsPath, kennel.CatsPrefix, kennel.DefaultCatSpritesheet)
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(createCatContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, kennel.ListUserCatsContinuation)(template.TemplateContinuation("kennel_cats.html", true), template.TemplateContinuation("kennel_cats.html", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("POST /kennel/cats/delete", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ deleteCatContinuation := kennel.RemoveCatContinuation(uploadAdapter, kennel.CatsPath)
+
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(deleteCatContinuation, FailurePassingContinuation)(kennel.ListUserCatsContinuation, FailurePassingContinuation)(template.TemplateContinuation("kennel_cats.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
+ })
+
+ mux.HandleFunc("GET /{template}", func(w http.ResponseWriter, r *http.Request) {
+ requestContext := makeRequestContext()
+ templateFile := r.PathValue("template")
+ LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(IdContinuation, IdContinuation)(template.TemplateContinuation(templateFile+".html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
return &http.Server{
diff --git a/database/kennel.go b/database/kennel.go
new file mode 100644
index 0000000..91525db
--- /dev/null
+++ b/database/kennel.go
@@ -0,0 +1,159 @@
+package database
+
+import (
+ "database/sql"
+ _ "github.com/mattn/go-sqlite3"
+ "log"
+ "time"
+)
+
+type KennelCat struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ Name string `json:"name"`
+ Link string `json:"link"`
+ Description string `json:"description"`
+ Spritesheet string `json:"spritesheet"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+type KennelState struct {
+ At time.Time `json:"at"`
+ EncodedState string `json:"state"`
+}
+
+func CountUserKennelCats(db *sql.DB, userID string) (int, error) {
+ log.Println("counting kennel cats for user", userID)
+
+ row := db.QueryRow("SELECT COUNT(*) FROM kennel_cat WHERE user_id = ?", userID)
+ var count int
+ err := row.Scan(&count)
+ if err != nil {
+ return 0, err
+ }
+ return count, nil
+}
+
+func GetUserKennelCats(db *sql.DB, userID string) ([]KennelCat, error) {
+ log.Println("getting kennel cats for user", userID)
+
+ rows, err := db.Query("SELECT * FROM kennel_cat WHERE user_id = ?", userID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var cats []KennelCat
+ for rows.Next() {
+ var cat KennelCat
+ err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ cats = append(cats, cat)
+ }
+
+ return cats, nil
+}
+
+func SaveKennelCat(db *sql.DB, cat *KennelCat) (*KennelCat, error) {
+ log.Println("saving kennel cat", cat.ID)
+
+ if (cat.CreatedAt == time.Time{}) {
+ cat.CreatedAt = time.Now()
+ }
+
+ _, err := db.Exec("INSERT OR REPLACE INTO kennel_cat (id, user_id, name, link, description, spritesheet, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", cat.ID, cat.UserID, cat.Name, cat.Link, cat.Description, cat.Spritesheet, cat.CreatedAt)
+
+ if err != nil {
+ return nil, err
+ }
+ return cat, nil
+}
+
+func GetKennelCat(db *sql.DB, catID string) (*KennelCat, error) {
+ log.Println("getting kennel cat", catID)
+
+ row := db.QueryRow("SELECT * FROM kennel_cat WHERE id = ?", catID)
+ var cat KennelCat
+ err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ return &cat, nil
+}
+
+func DeleteKennelCat(db *sql.DB, catID string) error {
+ log.Println("deleting kennel cat", catID)
+
+ _, err := db.Exec("DELETE FROM kennel_cat WHERE id = ?", catID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func GetRandomKennelCat(dbConn *sql.DB) (*KennelCat, error) {
+ log.Println("getting random kennel cat")
+
+ row := dbConn.QueryRow("SELECT * FROM kennel_cat ORDER BY RANDOM() LIMIT 1")
+ var cat KennelCat
+ err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ return &cat, nil
+}
+
+func GetNextKennelCat(dbConn *sql.DB, lastID string, next bool) (*KennelCat, error) {
+ log.Println("getting next kennel cat")
+
+ operation := ">"
+ sorted := "ASC"
+ if !next {
+ operation = "<"
+ sorted = "DESC"
+ }
+
+ row := dbConn.QueryRow("SELECT * FROM kennel_cat WHERE id "+operation+" ? ORDER BY id "+sorted+" LIMIT 1", lastID)
+
+ var cat KennelCat
+ err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
+ if err != nil {
+ if next {
+ // loop "back" to the first in the ring
+ row = dbConn.QueryRow("SELECT * FROM kennel_cat ORDER BY id ASC LIMIT 1")
+ } else {
+ // loop "forward" to the first in the ring
+ row = dbConn.QueryRow("SELECT * FROM kennel_cat ORDER BY id DESC LIMIT 1")
+ }
+ err = row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return &cat, nil
+}
+
+func GetKennel(dbConn *sql.DB) ([]KennelCat, error) {
+ log.Println("getting kennel")
+
+ rows, err := dbConn.Query("SELECT * FROM kennel_cat")
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var cats []KennelCat
+ for rows.Next() {
+ var cat KennelCat
+ err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ cats = append(cats, cat)
+ }
+
+ return cats, nil
+}
diff --git a/database/migrate.go b/database/migrate.go
index e9e21b7..0c8318c 100644
--- a/database/migrate.go
+++ b/database/migrate.go
@@ -162,6 +162,26 @@ func MigrateProfiles(dbConn *sql.DB) (*sql.DB, error) {
return dbConn, nil
}
+func MigrateKennel(dbConn *sql.DB) (*sql.DB, error) {
+ log.Println("migrating kennel tables")
+
+ _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS kennel_cat (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ user_id INTEGER NOT NULL,
+ link TEXT NOT NULL,
+ description TEXT NOT NULL,
+ spritesheet TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+ );`)
+ if err != nil {
+ return dbConn, err
+ }
+
+ return dbConn, nil
+}
+
func Migrate(dbConn *sql.DB) (*sql.DB, error) {
log.Println("migrating database")
@@ -173,6 +193,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) {
MigrateDNSRecords,
MigrateGuestBook,
MigrateProfiles,
+ MigrateKennel,
}
for _, migration := range migrations {
diff --git a/static/css/colors.css b/static/css/colors.css
index 46357d9..69b97db 100644
--- a/static/css/colors.css
+++ b/static/css/colors.css
@@ -7,6 +7,7 @@
--container-bg-light: #fff7f87a;
--border-color-light: #692fcc;
--error-color-light: #a83254;
+ --tr-color-light: #8bcefa;
--background-color-dark: #333;
--background-color-dark-2: #2c2c2c;
@@ -16,6 +17,7 @@
--container-bg-dark: #424242ea;
--border-color-dark: #956ade;
--error-color-dark: #851736;
+ --tr-color-dark: #212a6a;
}
[data-theme="DARK"] {
@@ -27,6 +29,7 @@
--border-color: var(--border-color-dark);
--error-color: var(--error-color-dark);
--confirm-color: var(--confirm-color-dark);
+ --tr-color: var(--tr-color-dark);
}
[data-theme="LIGHT"] {
@@ -38,6 +41,7 @@
--border-color: var(--border-color-light);
--error-color: var(--error-color-light);
--confirm-color: var(--confirm-color-light);
+ --tr-color: var(--tr-color-light);
}
.error {
diff --git a/static/css/table.css b/static/css/table.css
index 75a961d..854f591 100644
--- a/static/css/table.css
+++ b/static/css/table.css
@@ -26,6 +26,6 @@ tbody tr {
}
tbody tr:hover {
- background-color: #ff47daa0;
+ background-color: var(--tr-color);
color: #2a2a2a;
}
diff --git a/static/img/cat_spritesheets/default.gif b/static/img/cat_spritesheets/default.gif
new file mode 100644
index 0000000..0d264d8
--- /dev/null
+++ b/static/img/cat_spritesheets/default.gif
Binary files differ
diff --git a/templates/base.html b/templates/base.html
index 036a748..7486f45 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -35,6 +35,8 @@
<span> | </span>
<a href="/keys">api keys.</a>
<span> | </span>
+ <a href="/kennel/cats">kennel.</a>
+ <span> | </span>
<a href="/profile">{{ .User.DisplayName }}.</a>
<span> | </span>
<a href="/logout">logout.</a>
diff --git a/templates/dns.html b/templates/dns.html
index 2f3f0a7..e04cbfa 100644
--- a/templates/dns.html
+++ b/templates/dns.html
@@ -37,7 +37,7 @@
<p>note that the name <em>must</em> be a subdomain of <em>{{ .User.Username }}</em></p>
<hr>
<label for="type">type.</label>
- <input type="text" name="type" placeholder="CNAME"
+ <input type="text" name="type"
{{ if not .RecordForm }}
placeholder="CNAME"
{{ else }}
diff --git a/templates/kennel_cats.html b/templates/kennel_cats.html
new file mode 100644
index 0000000..ce31738
--- /dev/null
+++ b/templates/kennel_cats.html
@@ -0,0 +1,66 @@
+{{ define "content" }}
+ <table>
+ <tr>
+ <th>name.</th>
+ <th>link.</th>
+ <th>description.</th>
+ <th>spritesheet.</th>
+ <th>created at.</th>
+ <th>remove.</th>
+ </tr>
+ {{ if (eq (len .Cats) 0) }}
+ <tr>
+ <td colspan="6"><span class="blinky">no cats found</span></td>
+ </tr>
+ {{ end }}
+ {{ range $cat := .Cats }}
+ <tr>
+ <td>{{ $cat.Name }}</td>
+ <td><a href="{{ $cat.Link }}">{{ $cat.Link }}</a></td>
+ <td>{{ $cat.Description }}</td>
+ <td><a href="{{ $cat.Spritesheet }}"><img width="100" src="{{ $cat.Spritesheet }}"></a></td>
+ <td class="time">{{ $cat.CreatedAt }}</td>
+ <td>
+ <form method="POST" action="/kennel/cats/delete">
+ <input type="hidden" name="id" value="{{ $cat.ID }}" />
+ <input type="submit" value="remove." />
+ </form>
+ </td>
+ </tr>
+ {{ end }}
+ </table>
+ <br>
+ <form method="POST" action="/kennel/cats" class="form" enctype="multipart/form-data">
+ <h2>add cat.</h2>
+ <hr>
+ <label for="name">name.</label>
+ <input type="text" name="name" id="name"
+ {{ if not .CatForm }}
+ placeholder="wallace."
+ {{ else }}
+ value="{{ .CatForm.Name }}"
+ {{ end }}
+ />
+ <label for="description">description.</label>
+ <input type="text" name="description" id="description"
+ {{ if not .CatForm }}
+ placeholder="a cat."
+ {{ else }}
+ value="{{ .CatForm.Description }}"
+ {{ end }}
+ />
+ <label for="link">link.</label>
+ <input type="text" name="link" id="link"
+ {{ if not .CatForm }}
+ placeholder="https://hatecomputers.club"
+ {{ else }}
+ value="{{ .CatForm.Link }}"
+ {{ end }}/>
+
+ <label for="spritesheet" style="margin:0">spritesheet.</label>
+ <h6>if not specified, will use <a href="/static/img/cat_spritesheets/default.gif">the default</a>. check it out for the format we expect. max 50KB.</h6>
+ <input type="file" name="spritesheet" id="spritesheet" />
+
+ <input type="submit" value="mrow." />
+ </form>
+{{ end }}