kennel #13
			
				
			
		
		
		
	|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,195 @@ | |||
| package kennel | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"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 GetCatStateAtContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { | ||||
| 	return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { | ||||
| 		atLong, err := strconv.ParseInt(req.FormValue("at"), 10, 64) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			resp.WriteHeader(http.StatusBadRequest) | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		cats, err := database.GetKennelStateAt(context.DBConn, atLong) | ||||
| 		if err != nil { | ||||
| 			log.Println(err) | ||||
| 			resp.WriteHeader(http.StatusInternalServerError) | ||||
| 			return failure(context, req, resp) | ||||
| 		} | ||||
| 
 | ||||
| 		(*context.TemplateData)["EncodedState"] = cats.EncodedState | ||||
| 		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 | ||||
| } | ||||
							
								
								
									
										34
									
								
								api/serve.go
								
								
								
								
							
							
						
						
									
										34
									
								
								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,23 +173,28 @@ 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 /kennel", func(w http.ResponseWriter, r *http.Request) { | ||||
| 			requestContext := makeRequestContext() | ||||
| 			LogRequestContinuation(requestContext, r, w)(kennel.GetKennelStateContinuation, kennel.GetKennelStateContinuation)(template.TemplateContinuation("kennel.json", true), template.TemplateContinuation("kennel.json", true))(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) | ||||
| 		}) | ||||
| 	mux.HandleFunc("GET /kennel", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		requestContext := makeRequestContext() | ||||
| 		LogRequestContinuation(requestContext, r, w)(auth.VerifySessionContinuation, FailurePassingContinuation)(kennel.GetCatStateAtContinuation, FailurePassingContinuation)(template.TemplateContinuation("kennel_enc.json", false), 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.ListUserCats, auth.GoLoginContinuation)(template.TemplateContinuation("kennel_cats.html", true), 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", func(w http.ResponseWriter, r *http.Request) { | ||||
| 				requestContext := makeRequestContext() | ||||
| 				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("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() | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import ( | |||
| 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"` | ||||
|  | @ -45,7 +46,7 @@ func GetUserKennelCats(db *sql.DB, userID string) ([]KennelCat, error) { | |||
| 	var cats []KennelCat | ||||
| 	for rows.Next() { | ||||
| 		var cat KennelCat | ||||
| 		err := rows.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 		err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | @ -62,7 +63,7 @@ func SaveKennelCat(db *sql.DB, cat *KennelCat) (*KennelCat, error) { | |||
| 		cat.CreatedAt = time.Now() | ||||
| 	} | ||||
| 
 | ||||
| 	_, err := db.Exec("INSERT OR REPLACE INTO kennel_cat (id, user_id, link, description, spritesheet, created_at) VALUES (?, ?, ?, ?, ?, ?)", cat.ID, cat.UserID, cat.Link, cat.Description, cat.Spritesheet, cat.CreatedAt) | ||||
| 	_, 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 | ||||
|  | @ -75,7 +76,7 @@ func GetKennelCat(db *sql.DB, catID string) (*KennelCat, error) { | |||
| 
 | ||||
| 	row := db.QueryRow("SELECT * FROM kennel_cat WHERE id = ?", catID) | ||||
| 	var cat KennelCat | ||||
| 	err := row.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 	err := row.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -104,7 +105,7 @@ func GetKennel(dbConn *sql.DB) ([]KennelCat, error) { | |||
| 	var cats []KennelCat | ||||
| 	for rows.Next() { | ||||
| 		var cat KennelCat | ||||
| 		err := rows.Scan(&cat.ID, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 		err := rows.Scan(&cat.ID, &cat.Name, &cat.UserID, &cat.Link, &cat.Description, &cat.Spritesheet, &cat.CreatedAt) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | @ -114,10 +115,22 @@ func GetKennel(dbConn *sql.DB) ([]KennelCat, error) { | |||
| 	return cats, nil | ||||
| } | ||||
| 
 | ||||
| func GetKennelState(dbConn *sql.DB) (*KennelState, error) { | ||||
| func GetLatestKennelState(dbConn *sql.DB) (*KennelState, error) { | ||||
| 	log.Println("getting kennel state") | ||||
| 
 | ||||
| 	row := dbConn.QueryRow("SELECT * FROM kennel_state") | ||||
| 	row := dbConn.QueryRow("SELECT * FROM kennel_state ORDER BY at DESC LIMIT 1") | ||||
| 	var state KennelState | ||||
| 	err := row.Scan(&state.At, &state.EncodedState) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &state, nil | ||||
| } | ||||
| 
 | ||||
| func GetKennelStateAt(dbConn *sql.DB, at int64) (*KennelState, error) { | ||||
| 	log.Println("getting kennel state at", at) | ||||
| 
 | ||||
| 	row := dbConn.QueryRow("SELECT * FROM kennel_state WHERE at = ?", at) | ||||
| 	var state KennelState | ||||
| 	err := row.Scan(&state.At, &state.EncodedState) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -167,6 +167,7 @@ func MigrateKennel(dbConn *sql.DB) (*sql.DB, error) { | |||
| 
 | ||||
| 	_, 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, | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -68,3 +68,104 @@ hr { | |||
| .info:hover { | ||||
|   opacity: 0.8; | ||||
| } | ||||
| @import "/static/css/colors.css"; | ||||
| @import "/static/css/blinky.css"; | ||||
| @import "/static/css/table.css"; | ||||
| @import "/static/css/form.css"; | ||||
| @import "/static/css/guestbook.css"; | ||||
| @import "/static/css/club.css"; | ||||
| 
 | ||||
| @font-face { | ||||
|   font-family: "ComicSans"; | ||||
|   src: url("/static/font/comicsans.ttf"); | ||||
| } | ||||
| 
 | ||||
| * { | ||||
|   box-sizing: border-box; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   color: var(--text-color); | ||||
|   font-family: "ComicSans", sans-serif; | ||||
| } | ||||
| 
 | ||||
| /* i just cannot get this to look good on firefox... */ | ||||
| @supports not (-moz-appearance: none) { | ||||
|   * { | ||||
|     cursor: url("/static/img/cursor-2.png"), auto; | ||||
|     -webkit-animation: cursor 400ms infinite; | ||||
|     animation: cursor 400ms infinite; | ||||
|   } | ||||
| 
 | ||||
|   @-webkit-keyframes cursor { | ||||
|     0% { | ||||
|       cursor: url("/static/img/cursor-2.png"), auto; | ||||
|     } | ||||
|     50% { | ||||
|       cursor: url("/static/img/cursor-1.png"), auto; | ||||
|     } | ||||
|     100% { | ||||
|       cursor: url("/static/img/cursor-2.png"), auto; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @keyframes cursor { | ||||
|     0% { | ||||
|       cursor: url("/static/img/cursor-2.png"), auto; | ||||
|     } | ||||
|     50% { | ||||
|       cursor: url("/static/img/cursor-1.png"), auto; | ||||
|     } | ||||
|     100% { | ||||
|       cursor: url("/static/img/cursor-2.png"), auto; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|   background-color: var(--background-color); | ||||
|   background-image: url("/static/img/stars.gif"); | ||||
|   min-height: 100vh; | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|   color: var(--link-color); | ||||
|   text-decoration: none; | ||||
|   font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|   text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
|   max-width: 1600px; | ||||
|   margin: auto; | ||||
|   background-color: var(--container-bg); | ||||
|   padding: 1rem; | ||||
| } | ||||
| 
 | ||||
| hr { | ||||
|   border: 0; | ||||
|   border-top: 1px solid var(--text-color); | ||||
| 
 | ||||
|   margin: 20px 0; | ||||
| } | ||||
| 
 | ||||
| .blinkies { | ||||
|   display: flex; | ||||
|   justify-content: left; | ||||
|   flex-wrap: wrap; | ||||
|   max-width: 900px; | ||||
|   gap: 10px 10px; | ||||
| } | ||||
| 
 | ||||
| .info { | ||||
|   margin-bottom: 1rem; | ||||
|   max-width: 600px; | ||||
| 
 | ||||
|   transition: opacity 0.3s; | ||||
| } | ||||
| 
 | ||||
| .info:hover { | ||||
|   opacity: 0.8; | ||||
| } | ||||
|  |  | |||
|  | @ -26,6 +26,6 @@ tbody tr { | |||
| } | ||||
| 
 | ||||
| tbody tr:hover { | ||||
|   background-color: #ff47daa0; | ||||
|   background-color: var(--tr-color); | ||||
|   color: #2a2a2a; | ||||
| } | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6.4 KiB | 
|  | @ -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> | ||||
|  |  | |||
|  | @ -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 }} | ||||
|  |  | |||
|  | @ -1,81 +0,0 @@ | |||
| {{ define "content" }} | ||||
|   <table> | ||||
|     <tr> | ||||
|       <th>type.</th> | ||||
|       <th>name.</th> | ||||
|       <th>content.</th> | ||||
|       <th>ttl.</th> | ||||
|       <th>internal.</th> | ||||
|       <th>created.</th> | ||||
|       <th>delete.</th> | ||||
|     </tr> | ||||
|     {{ if (eq (len .DNSRecords) 0) }} | ||||
|     <tr> | ||||
|       <td colspan="7"><span class="blinky">no dns records found.</span></td> | ||||
|     </tr> | ||||
|     {{ end }} | ||||
|     {{ range $record := .DNSRecords }} | ||||
|       <tr> | ||||
| 	<td>{{ $record.Type }}</td> | ||||
| 	<td>{{ $record.Name }}</td> | ||||
| 	<td>{{ $record.Content }}</td> | ||||
| 	<td>{{ $record.TTL }}</td> | ||||
| 	<td>{{ $record.Internal }}</td> | ||||
| 	<td class="time">{{ $record.CreatedAt }}</td> | ||||
| 	<td> | ||||
| 	  <form method="POST" action="/dns/delete"> | ||||
| 	    <input type="hidden" name="id" value="{{ $record.ID }}" /> | ||||
| 	    <input type="submit" value="Delete" /> | ||||
| 	  </form> | ||||
| 	</td> | ||||
|       </tr> | ||||
|     {{ end }} | ||||
|   </table> | ||||
|   <br> | ||||
|   <form method="POST" action="/dns" class="form"> | ||||
|     <h2>add dns records.</h2> | ||||
|     <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" | ||||
| 	   {{ if not .RecordForm }} | ||||
| 	   placeholder="CNAME" | ||||
| 	   {{ else }} | ||||
| 	   value="{{ .RecordForm.Type }}" | ||||
| 	   {{ end }} | ||||
| 	   required /> | ||||
|     <label for="name">name.</label> | ||||
|     <input type="text" name="name" | ||||
| 	   {{ if not .RecordForm }} | ||||
| 	   placeholder="{{ .User.Username }} || endpoint.{{ .User.Username }}..." | ||||
| 	   {{ else }} | ||||
| 	   value="{{ .RecordForm.Name }}" | ||||
| 	   {{ end }} | ||||
| 	   required/> | ||||
|     <label for="content">content.</label> | ||||
|     <input type="text" name="content" | ||||
| 	   {{ if not .RecordForm }} | ||||
| 	   placeholder="{{ .User.Username }}.dev" | ||||
| 	   {{ else }} | ||||
| 	   value="{{ .RecordForm.Content }}" | ||||
| 	   {{ end }} | ||||
| 	   required /> | ||||
|     <label for="ttl">ttl.</label> | ||||
|     <input type="text" name="ttl" | ||||
| 	   {{ if not .RecordForm }} | ||||
| 	   placeholder="43200" | ||||
| 	   {{ else }} | ||||
| 	   value="{{ .RecordForm.TTL }}" | ||||
| 	   {{ end }} | ||||
| 	   required /> | ||||
|     <label for="internal"> | ||||
|       internal. | ||||
|       <input style='display:inline;width:auto;' type="checkbox" name="internal" | ||||
| 	   {{ if .RecordForm.Internal }} | ||||
| 	   checked | ||||
| 	   {{ end }} | ||||
| 	   /> | ||||
|     </label> | ||||
|     <input type="submit" value="add." /> | ||||
|   </form> | ||||
| {{ end }} | ||||
|  | @ -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 }} | ||||
|  | @ -0,0 +1 @@ | |||
| {{ define "content" }}{{ .EncodedState }}{{ end }} | ||||
		Loading…
	
		Reference in New Issue