| @@ -1,8 +1,74 @@ | ||
| package model | ||
|
|
||
| import ( | ||
| "log" | ||
|
|
||
| "golang.org/x/crypto/bcrypt" | ||
| ) | ||
|
|
||
| // UserDatastore describes methods available to our database | ||
| // pertaining to users | ||
| type UserDatastore interface { | ||
| GetUserByUsername(name string) (*User, bool) | ||
| GetUserByID(id int) (*User, bool) | ||
| CreateUser(name, password string) (*User, bool) | ||
| } | ||
|
|
||
| // User represents a user in our application | ||
| type User struct { | ||
| ID int `db:"id"` | ||
| Username string `db:"username"` | ||
| Password []byte `db:"password"` | ||
| } | ||
|
|
||
| // GetUserByUsername returns a user object with matching | ||
| // username | ||
| func (db *DB) GetUserByUsername(name string) (*User, bool) { | ||
| u := &User{} | ||
| err := db.Get(u, `SELECT id, username, password | ||
| FROM User | ||
| WHERE username = ?`, | ||
| name) | ||
| if err != nil { | ||
| return nil, false | ||
| } | ||
| return u, true | ||
| } | ||
|
|
||
| // GetUserByID returns a user object with matchin | ||
| // id if found | ||
| func (db *DB) GetUserByID(id int) (*User, bool) { | ||
| u := &User{} | ||
| err := db.Get(u, `SELECT id, username, password | ||
| FROM User | ||
| WHERE id=?`, id) | ||
| if err != nil { | ||
| return nil, false | ||
| } | ||
| return u, true | ||
| } | ||
|
|
||
| // CreateUser adds a user to our database | ||
| func (db *DB) CreateUser(name, password string) (*User, bool) { | ||
| p, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||
| if err != nil { | ||
| log.Printf("CreateUser: %s", err.Error()) | ||
| return nil, false | ||
| } | ||
| res, err := db.Exec(`INSERT INTO `+"`User`"+` (username, password) VALUES (?, ?)`, | ||
| name, p) | ||
| if err != nil { | ||
| log.Printf("CreateUser: %s", err.Error()) | ||
| return nil, false | ||
| } | ||
| id, err := res.LastInsertId() | ||
| if err != nil { | ||
| log.Printf("CreateUser: %s", err.Error()) | ||
| return nil, false | ||
| } | ||
| user := &User{ | ||
| ID: int(id), | ||
| Username: name, | ||
| } | ||
| return user, true | ||
| } |
| @@ -0,0 +1,141 @@ | ||
| package routes | ||
|
|
||
| import ( | ||
| "context" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
| "time" | ||
|
|
||
| jwt "github.com/dgrijalva/jwt-go" | ||
| "golang.org/x/crypto/bcrypt" | ||
| ) | ||
|
|
||
| // Claims is a struct that represents the data stored in our | ||
| // tokens | ||
| type Claims struct { | ||
| UID int `json:"uid"` | ||
| Username string `json:"username"` | ||
| jwt.StandardClaims | ||
| } | ||
|
|
||
| func (env *Env) loginIndex(w http.ResponseWriter, r *http.Request) { | ||
| // already authenticated | ||
| if claims := r.Context().Value("Claims"); claims != nil { | ||
| http.Redirect(w, r, "/", http.StatusSeeOther) | ||
| return | ||
| } | ||
|
|
||
| errs := r.Context().Value("Errors") | ||
| log.Println(errs) | ||
| // We keep claims here because base template requires at least a nil claims | ||
| data := map[string]interface{}{ | ||
| "Claims": nil, | ||
| "Errors": errs, | ||
| } | ||
|
|
||
| if t, ok := tmpls["login.html"]; ok { | ||
| t.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) loginProcess(w http.ResponseWriter, r *http.Request) { | ||
| u := r.PostFormValue("username") | ||
| p := r.PostFormValue("password") | ||
|
|
||
| if u == "" || p == "" { | ||
| loginPageWithErrors(w, r, "Username or Password cannot be empty.") | ||
| return | ||
| } | ||
|
|
||
| //Authenticate user | ||
| user, ok := env.db.GetUserByUsername(u) | ||
| if !ok { | ||
| loginPageWithErrors(w, r, "Invalid Username or Password") | ||
| return | ||
| } | ||
|
|
||
| if err := bcrypt.CompareHashAndPassword(user.Password, []byte(p)); err != nil { | ||
| // Invalid password | ||
| loginPageWithErrors(w, r, "Invalid Username or Password") | ||
| return | ||
|
|
||
| } | ||
| assignToken(w, user.ID, user.Username) | ||
|
|
||
| http.Redirect(w, r, "/", http.StatusFound) | ||
| return | ||
| } | ||
|
|
||
| // Logs a user out by "deleting" their token | ||
| func (env *Env) logoutProcess(w http.ResponseWriter, r *http.Request) { | ||
| deleteCookie := http.Cookie{Name: "Auth", Value: "none", Expires: time.Now()} | ||
| http.SetCookie(w, &deleteCookie) | ||
| http.Redirect(w, r, "/", http.StatusFound) | ||
| } | ||
|
|
||
| // little utility | ||
| func loginPageWithErrors(w http.ResponseWriter, r *http.Request, errs ...string) { | ||
| r.Method = "GET" | ||
| ctx := context.WithValue(r.Context(), "Errors", errs) | ||
| http.Redirect(w, r.WithContext(ctx), "/login", http.StatusSeeOther) | ||
| return | ||
| } | ||
|
|
||
| // Creates a new user | ||
| func (env *Env) registerProcess(w http.ResponseWriter, r *http.Request) { | ||
| u := r.PostFormValue("username") | ||
| p := r.PostFormValue("password") | ||
| pc := r.PostFormValue("confirm-password") | ||
|
|
||
| if u == "" || p == "" || pc == "" { | ||
| loginPageWithErrors(w, r, "Username or Password cannot be empty.") | ||
| return | ||
| } | ||
|
|
||
| if p != pc { | ||
| loginPageWithErrors(w, r, "Passwords do not match.") | ||
| } | ||
|
|
||
| if user, ok := env.db.CreateUser(u, p); ok { | ||
| log.Println("User created") | ||
| assignToken(w, user.ID, user.Username) | ||
| http.Redirect(w, r, "/", http.StatusFound) | ||
| return | ||
| } | ||
|
|
||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
|
|
||
| func assignToken(w http.ResponseWriter, id int, uname string) { | ||
| expireToken := time.Now().Add(time.Hour * 1).Unix() | ||
| expireCookie := time.Now().Add(time.Hour * 1) | ||
|
|
||
| claims := Claims{ | ||
| id, | ||
| uname, | ||
| jwt.StandardClaims{ | ||
| ExpiresAt: expireToken, | ||
| Issuer: "localhost:8081", | ||
| }, | ||
| } | ||
|
|
||
| // Generate signed token with our claims | ||
| token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
| signedToken, err := token.SignedString([]byte(os.Getenv("TOKEN_SIGNING_KEY"))) | ||
| if err != nil { | ||
| http.Error(w, "Error creating signed token", http.StatusInternalServerError) | ||
| } | ||
|
|
||
| // Store our token on client | ||
| cookie := http.Cookie{ | ||
| Name: "Auth", | ||
| Value: signedToken, | ||
| Expires: expireCookie, | ||
| HttpOnly: true, | ||
| } | ||
| http.SetCookie(w, &cookie) | ||
|
|
||
| } |
| @@ -0,0 +1,112 @@ | ||
| package routes | ||
|
|
||
| import ( | ||
| "log" | ||
| "net/http" | ||
| "strconv" | ||
|
|
||
| "github.com/gorilla/mux" | ||
| "github.com/murder-hobos/murder-hobos/model" | ||
| "github.com/murder-hobos/murder-hobos/util" | ||
| ) | ||
|
|
||
| func (env *Env) characterIndex(w http.ResponseWriter, r *http.Request) { | ||
| c := r.Context().Value("Claims") | ||
| claims := c.(Claims) | ||
|
|
||
| chars, err := env.db.GetAllCharacters(claims.UID) | ||
| if err != nil && err != model.ErrNoResult { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| "Characters": chars, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["characters.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| } | ||
|
|
||
| // Information about specific character | ||
| func (env *Env) characterDetails(w http.ResponseWriter, r *http.Request) { | ||
| c := r.Context().Value("Claims") | ||
| claims := c.(Claims) | ||
| name := mux.Vars(r)["characterName"] | ||
|
|
||
| char := &model.Character{} | ||
| c, err := env.db.GetCharacterByName(claims.UID, name) | ||
| if err != nil { | ||
| log.Printf("Error getting Character with name: %s\n", name) | ||
| log.Printf(err.Error()) | ||
| errorHandler(w, r, http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| "Character": char, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["character-details.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for class-details\n") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) newCharacterIndex(w http.ResponseWriter, r *http.Request) { | ||
| claims, _ := r.Context().Value("Claims").(Claims) | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["character-creator.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| log.Println("EXECUTED") | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for character-creator\n") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) newCharacterProcess(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims").(Claims) | ||
|
|
||
| name := r.PostFormValue("name") | ||
| //class := r.PostFormValue("class") | ||
| //level := r.PostFormValue("level") | ||
| race := r.PostFormValue("race") | ||
| a, _ := strconv.Atoi(r.PostFormValue("abilityMod")) | ||
| p, _ := strconv.Atoi(r.PostFormValue("profBonus")) | ||
| ability := util.ToNullInt64(a) | ||
| proficiency := util.ToNullInt64(p) | ||
|
|
||
| char := &model.Character{ | ||
| Name: name, | ||
| Race: race, | ||
| SpellAbilityModifier: ability, | ||
| ProficienyBonus: proficiency, | ||
| UserID: claims.UID, | ||
| } | ||
|
|
||
| if _, err := env.db.CreateCharacter(claims.UID, char); err != nil { | ||
| log.Printf("CreateCharacter: %s\n", err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| // if _, err := env.db.SetCharacterLevel(charID, className, level int); err != nil { | ||
| // errorHandler(w,r,http.StatusInternalServerError) | ||
| // } | ||
| r.Method = "GET" | ||
| http.Redirect(w, r, "/user/character", http.StatusFound) | ||
| } |
| @@ -0,0 +1,68 @@ | ||
| package routes | ||
|
|
||
| import ( | ||
| "log" | ||
| "net/http" | ||
|
|
||
| "github.com/gorilla/mux" | ||
| ) | ||
|
|
||
| // lists all classes | ||
| func (env *Env) classIndex(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims") | ||
|
|
||
| cs, err := env.db.GetAllClasses() | ||
| if err != nil { | ||
| log.Println("Classes handler: " + err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| "Classes": cs, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["classes.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for classes\n") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| // Shows a list of all spells available to a class | ||
| func (env *Env) classDetails(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims") | ||
| name := mux.Vars(r)["className"] | ||
|
|
||
| class, err := env.db.GetClassByName(name) | ||
| if err != nil { | ||
| log.Printf("Error getting Class by name: %s\n", name) | ||
| log.Printf(err.Error()) | ||
| errorHandler(w, r, http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
||
| spells, err := env.db.GetClassSpells(class.ID) | ||
| if err != nil { | ||
| log.Println("Class-detail handler" + err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| "Class": class, | ||
| "Spells": spells, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["class-details.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for class-details\n") | ||
| return | ||
| } | ||
| } |
| @@ -1 +1,53 @@ | ||
| package routes | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
|
|
||
| jwt "github.com/dgrijalva/jwt-go" | ||
| ) | ||
|
|
||
| // withClaims checks the request for a valid auth token. | ||
| // If valid, the Claims object is added to the request's context | ||
| func (env *Env) withClaims(fn http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| cookie, err := r.Cookie("Auth") | ||
| if err != nil { | ||
| fn.ServeHTTP(w, r) | ||
| return | ||
| } | ||
|
|
||
| token, err := jwt.ParseWithClaims(cookie.Value, &Claims{}, func(token *jwt.Token) (interface{}, error) { | ||
| // Make sure token's signature wasn't changed | ||
| if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||
| return nil, fmt.Errorf("Unexpected siging method") | ||
| } | ||
| return []byte(os.Getenv("TOKEN_SIGNING_KEY")), nil | ||
| }) | ||
|
|
||
| if claims, ok := token.Claims.(*Claims); ok && token.Valid { | ||
| ctx := context.WithValue(r.Context(), "Claims", *claims) | ||
| fn.ServeHTTP(w, r.WithContext(ctx)) | ||
| } else { | ||
| fn.ServeHTTP(w, r) | ||
| return | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| func (env *Env) authRequired(fn http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if c := r.Context().Value("Claims"); c != nil { | ||
| if _, ok := c.(Claims); ok { | ||
| log.Println("AUTHED") | ||
| fn.ServeHTTP(w, r) | ||
| } | ||
| } else { | ||
| http.Redirect(w, r, "/", http.StatusUnauthorized) | ||
| return | ||
| } | ||
| }) | ||
| } |
| @@ -0,0 +1,124 @@ | ||
| package routes | ||
|
|
||
| import ( | ||
| "log" | ||
| "net/http" | ||
|
|
||
| "github.com/gorilla/mux" | ||
| "github.com/murder-hobos/murder-hobos/model" | ||
| ) | ||
|
|
||
| func (env *Env) spellIndex(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims") | ||
|
|
||
| spells, err := env.db.GetAllCannonSpells() | ||
| if err != nil { | ||
| http.Error(w, err.Error(), http.StatusInternalServerError) | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spells": spells, | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["spells.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) spellFilter(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims") | ||
|
|
||
| level := r.FormValue("level") | ||
| school := r.FormValue("school") | ||
|
|
||
| spells, err := env.db.FilterCannonSpells(level, school) | ||
| if err != nil { | ||
| if err == model.ErrNoResult { | ||
| // do nothing, just show no results on page (already in template) | ||
| } else { // something happened | ||
| log.Printf("routes - cannonSpells: Error filtering cannon spells: %s\n", err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spells": spells, | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["spells.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) spellSearch(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims") | ||
| name := r.FormValue("name") | ||
|
|
||
| spells, err := env.db.SearchCannonSpells(name) | ||
| if err != nil { | ||
| if err == model.ErrNoResult { | ||
| // do nothing, just show no results on page (already in template) | ||
| } else { // something happened | ||
| log.Printf("routes - cannonSpells: Error filtering cannon spells: %s\n", err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spells": spells, | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["spells.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
|
||
| // Show information about a single spell | ||
| func (env *Env) spellDetails(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims") | ||
| name := mux.Vars(r)["spellName"] | ||
|
|
||
| spell, err := env.db.GetCannonSpellByName(name) | ||
| if err != nil { | ||
| log.Printf("Error getting spell by name: %s\n", name) | ||
| log.Printf(err.Error()) | ||
| errorHandler(w, r, http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
||
| classes, err := env.db.GetSpellClasses(spell.ID) | ||
| // we shouldn't have an error at this point, we should have a spell | ||
| if err != nil { | ||
| log.Printf("Error getting spell classes with id %d\n", spell.ID) | ||
| log.Println(err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spell": spell, | ||
| "Classes": classes, | ||
| "Claims": claims, | ||
| "IsCannon": true, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["spell-details.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for spell-details\n") | ||
| return | ||
| } | ||
| } |
| @@ -0,0 +1,207 @@ | ||
| package routes | ||
|
|
||
| import ( | ||
| "log" | ||
| "net/http" | ||
| "strconv" | ||
|
|
||
| "github.com/gorilla/mux" | ||
| "github.com/murder-hobos/murder-hobos/model" | ||
| "github.com/murder-hobos/murder-hobos/util" | ||
| ) | ||
|
|
||
| func (env *Env) userSpellIndex(w http.ResponseWriter, r *http.Request) { | ||
| c := r.Context().Value("Claims") | ||
| claims := c.(Claims) | ||
|
|
||
| spells, err := env.db.GetAllUserSpells(claims.UID) | ||
| if err != nil && err != model.ErrNoResult { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| "Spells": spells, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["user-spells.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) userSpellFilter(w http.ResponseWriter, r *http.Request) { | ||
| c := r.Context().Value("Claims") | ||
| claims := c.(Claims) | ||
|
|
||
| level := r.FormValue("level") | ||
| school := r.FormValue("school") | ||
|
|
||
| spells, err := env.db.FilterUserSpells(claims.UID, level, school) | ||
| if err != nil { | ||
| if err == model.ErrNoResult { | ||
| // do nothing, just show no results on page (already in template) | ||
| } else { // something happened | ||
| log.Printf("routes - userSpells: Error filtering cannon spells: %s\n", err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spells": spells, | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["user-spells.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) userSpellSearch(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims").(Claims) | ||
| name := r.FormValue("name") | ||
|
|
||
| spells, err := env.db.SearchUserSpells(claims.UID, name) | ||
| if err != nil { | ||
| if err == model.ErrNoResult { | ||
| // do nothing, just show no results on page (already in template) | ||
| } else { // something happened | ||
| log.Printf("routes - cannonSpells: Error filtering cannon spells: %s\n", err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spells": spells, | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["user-spells.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
|
||
| // Show information about a single spell | ||
| func (env *Env) userSpellDetails(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims").(Claims) | ||
| name := mux.Vars(r)["spellName"] | ||
|
|
||
| spell, err := env.db.GetUserSpellByName(claims.UID, name) | ||
| if err != nil { | ||
| log.Printf("Error getting spell by name: %s\n", name) | ||
| log.Printf(err.Error()) | ||
| errorHandler(w, r, http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
||
| classes, err := env.db.GetSpellClasses(spell.ID) | ||
| // we shouldn't have an error at this point, we should have a spell | ||
| if err != nil { | ||
| log.Printf("Error getting spell classes with id %d\n", spell.ID) | ||
| log.Println(err.Error()) | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Spell": spell, | ||
| "Classes": classes, | ||
| "Claims": claims, | ||
| "IsUser": true, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["spell-details.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for spell-details\n") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) newSpellProcess(w http.ResponseWriter, r *http.Request) { | ||
| claims := r.Context().Value("Claims").(Claims) | ||
|
|
||
| r.ParseForm() | ||
| name := r.FormValue("name") | ||
| school := r.FormValue("school") | ||
| level := r.FormValue("level") | ||
| castTime := r.FormValue("castTime") | ||
| duration := r.FormValue("duration") | ||
| ran := r.FormValue("range") | ||
| verbal, _ := strconv.ParseBool(r.FormValue("verbal")) | ||
| somatic, _ := strconv.ParseBool(r.FormValue("somatic")) | ||
| material, _ := strconv.ParseBool(r.FormValue("material")) | ||
| materialDesc := util.ToNullString(r.FormValue("materialDesc")) | ||
| conc, _ := strconv.ParseBool(r.FormValue("concentration")) | ||
| ritual, _ := strconv.ParseBool(r.FormValue("ritual")) | ||
| desc := r.FormValue("spellDesc") | ||
| sourceID := claims.UID | ||
|
|
||
| spell := &model.Spell{ | ||
| 0, | ||
| name, | ||
| level, | ||
| school, | ||
| castTime, | ||
| duration, | ||
| ran, | ||
| verbal, | ||
| somatic, | ||
| material, | ||
| materialDesc, | ||
| conc, | ||
| ritual, | ||
| desc, | ||
| sourceID, | ||
| } | ||
| if _, err := env.db.CreateSpell(claims.UID, *spell); err != nil { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Println(err.Error()) | ||
| return | ||
| } | ||
| http.Redirect(w, r, "/user/spell", http.StatusFound) | ||
| } | ||
|
|
||
| func (env *Env) newSpellIndex(w http.ResponseWriter, r *http.Request) { | ||
| log.Println("newSpellIndex") | ||
| claims, _ := r.Context().Value("Claims").(Claims) | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["spell-creator.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| log.Println("EXECUTED") | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for spell-creator\n") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| func (env *Env) userProfileIndex(w http.ResponseWriter, r *http.Request) { | ||
| claims, _ := r.Context().Value("Claims").(Claims) | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Claims": claims, | ||
| } | ||
|
|
||
| if tmpl, ok := env.tmpls["profile.html"]; ok { | ||
| tmpl.ExecuteTemplate(w, "base", data) | ||
| } else { | ||
| errorHandler(w, r, http.StatusInternalServerError) | ||
| log.Printf("Error loading template for spell-creator\n") | ||
| return | ||
| } | ||
|
|
||
| } |
| @@ -0,0 +1,113 @@ | ||
| body { | ||
| padding-top: 90px; | ||
| } | ||
|
|
||
| .panel-login { | ||
| border-color: #ccc; | ||
| -webkit-box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.2); | ||
| -moz-box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.2); | ||
| box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.2); | ||
| } | ||
|
|
||
| .panel-login>.panel-heading { | ||
| color: #00415d; | ||
| background-color: #fff; | ||
| border-color: #fff; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .panel-login>.panel-heading a { | ||
| text-decoration: none; | ||
| color: #666; | ||
| font-weight: bold; | ||
| font-size: 15px; | ||
| -webkit-transition: all 0.1s linear; | ||
| -moz-transition: all 0.1s linear; | ||
| transition: all 0.1s linear; | ||
| } | ||
|
|
||
| .panel-login>.panel-heading a.active { | ||
| color: #029f5b; | ||
| font-size: 18px; | ||
| } | ||
|
|
||
| .panel-login>.panel-heading hr { | ||
| margin-top: 10px; | ||
| margin-bottom: 0px; | ||
| clear: both; | ||
| border: 0; | ||
| height: 1px; | ||
| background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0)); | ||
| background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0)); | ||
| background-image: -ms-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0)); | ||
| background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0)); | ||
| } | ||
|
|
||
| .panel-login input[type="text"], | ||
| .panel-login input[type="email"], | ||
| .panel-login input[type="password"] { | ||
| height: 45px; | ||
| border: 1px solid #ddd; | ||
| font-size: 16px; | ||
| -webkit-transition: all 0.1s linear; | ||
| -moz-transition: all 0.1s linear; | ||
| transition: all 0.1s linear; | ||
| } | ||
|
|
||
| .panel-login input:hover, | ||
| .panel-login input:focus { | ||
| outline: none; | ||
| -webkit-box-shadow: none; | ||
| -moz-box-shadow: none; | ||
| box-shadow: none; | ||
| border-color: #ccc; | ||
| } | ||
|
|
||
| .btn-login { | ||
| background-color: #59B2E0; | ||
| outline: none; | ||
| color: #fff; | ||
| font-size: 14px; | ||
| height: auto; | ||
| font-weight: normal; | ||
| padding: 14px 0; | ||
| text-transform: uppercase; | ||
| border-color: #59B2E6; | ||
| } | ||
|
|
||
| .btn-login:hover, | ||
| .btn-login:focus { | ||
| color: #fff; | ||
| background-color: #53A3CD; | ||
| border-color: #53A3CD; | ||
| } | ||
|
|
||
| .forgot-password { | ||
| text-decoration: underline; | ||
| color: #888; | ||
| } | ||
|
|
||
| .forgot-password:hover, | ||
| .forgot-password:focus { | ||
| text-decoration: underline; | ||
| color: #666; | ||
| } | ||
|
|
||
| .btn-register { | ||
| background-color: #1CB94E; | ||
| outline: none; | ||
| color: #fff; | ||
| font-size: 14px; | ||
| height: auto; | ||
| font-weight: normal; | ||
| padding: 14px 0; | ||
| text-transform: uppercase; | ||
| border-color: #1CB94A; | ||
| } | ||
|
|
||
| .btn-register:hover, | ||
| .btn-register:focus { | ||
| color: #fff; | ||
| background-color: #1CA347; | ||
| border-color: #1CA347; | ||
| } |
| @@ -0,0 +1,18 @@ | ||
| $(function () { | ||
|
|
||
| $('#login-form-link').click(function (e) { | ||
| $("#login-form").delay(100).fadeIn(100); | ||
| $("#register-form").fadeOut(100); | ||
| $('#register-form-link').removeClass('active'); | ||
| $(this).addClass('active'); | ||
| e.preventDefault(); | ||
| }); | ||
| $('#register-form-link').click(function (e) { | ||
| $("#register-form").delay(100).fadeIn(100); | ||
| $("#login-form").fadeOut(100); | ||
| $('#login-form-link').removeClass('active'); | ||
| $(this).addClass('active'); | ||
| e.preventDefault(); | ||
| }); | ||
|
|
||
| }); |
| @@ -0,0 +1,87 @@ | ||
| {{define "title"}}Create Character - Murder Hobos{{end}} {{define "content"}} | ||
| <br> | ||
| <div class="page-header"> | ||
| <h1>Character Creator</h1> | ||
| </div> | ||
| <div> | ||
| <form class="form" method="POST"> | ||
| <div class="form-group" name="nameEntry"> | ||
| <label>Name: </label> | ||
| <input type="text" name="name"></input> | ||
| </div> | ||
| <div class="form-group" name="classSelection"> | ||
| <label>Class: </label> | ||
| <select class="form-control" name="class"> | ||
| <option selected disabled value="">Select Class</option> | ||
| <option value="Bard">Bard</option> | ||
| <option value="Cleric">Cleric</option> | ||
| <option value="Druid">Druid</option> | ||
| <option value="Ranger">Ranger</option> | ||
| <option value="Sorcerer">Sorcerer</option> | ||
| <option value="Warlock">Warlock</option> | ||
| <option value="Wizard">Wizard</option> | ||
| <option value="Fighter">Fighter</option> | ||
| <option value="Pogue">Rogue</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group" name="LevelSelection"> | ||
| <label>Level: </label> | ||
| <select class="form-control" name="level"> | ||
| <option selected disabled value="">Select Level</option> | ||
| <option value="1">Level 1</option> | ||
| <option value="2">Level 2</option> | ||
| <option value="3">Level 3</option> | ||
| <option value="4">Level 4</option> | ||
| <option value="5">Level 5</option> | ||
| <option value="6">Level 6</option> | ||
| <option value="7">Level 7</option> | ||
| <option value="8">Level 8</option> | ||
| <option value="9">Level 9</option> | ||
| <option value="10">Level 10</option> | ||
| <option value="11">Level 11</option> | ||
| <option value="12">Level 12</option> | ||
| <option value="13">Level 13</option> | ||
| <option value="14">Level 14</option> | ||
| <option value="15">Level 15</option> | ||
| <option value="16">Level 16</option> | ||
| <option value="17">Level 17</option> | ||
| <option value="18">Level 18</option> | ||
| <option value="19">Level 19</option> | ||
| <option value="20">Level 20</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group" name="raceEntry"> | ||
| <label>Race: </label> | ||
| <input type="text" name="race"></input> | ||
| </div> | ||
| <div class="form-group" name="abilityModifier"> | ||
| <label>Ability Modifier: </label> | ||
| <select class="form-control" name="abilityMod"> | ||
| <option selected disabled value="">Select Ability Modifier</option> | ||
| <option value="-3">-3</option> | ||
| <option value="-2">-2</option> | ||
| <option value="-1">-1</option> | ||
| <option value="0">0</option> | ||
| <option value="1">+1</option> | ||
| <option value="2">+2</option> | ||
| <option value="3">+3</option> | ||
| <option value="4">+4</option> | ||
| <option value="5">+5</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group" name="profieBonus"> | ||
| <label>Proficiency Bonus: </label> | ||
| <select class="form-control" name="profBonus"> | ||
| <option selected disabled value="">Select Proficiency Bonus</option> | ||
| <option value="1">1</option> | ||
| <option value="2">2</option> | ||
| <option value="3">3</option> | ||
| <option value="4">4</option> | ||
| <option value="5">5</option> | ||
| <option value="6">6</option> | ||
| </select> | ||
| </div> | ||
| <input class="btn btn-primary" type="submit" value="Create Character"></input> | ||
| </form> | ||
| </div> | ||
| {{end}} {{define "scripts"}}{{end}} |
| @@ -0,0 +1,19 @@ | ||
| {{define "title"}}{{.Character.Name}} - Murder Hobos{{end}} {{define "content"}} | ||
| <br> | ||
| <div class="container"> | ||
| <div class="page-header"> | ||
| <h1>The amazing <em><strong>{{.Character.Name}}</strong></em></h1> | ||
| </div> | ||
| <div class="row"> | ||
| <div class="col-lg-6 col-md-6 col-sm-8 col-xs-8"> | ||
| <div class="list-type"> | ||
| <ul> | ||
| <li><strong>Race: </strong>{{.Character.Race}}</li> | ||
| <li><strong>Spell Ability Modifier: </strong>{{.Character.SpellAbilityModifier}}</li> | ||
| <li><strong>Proficiency Bonus: </strong>{{.Character.ProficienyBonus}}</li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {{end}} |
| @@ -0,0 +1,24 @@ | ||
| {{define "title"}}Murder Hobos{{end}} {{define "content"}} | ||
| <br> | ||
| <div class="container"> | ||
| <div class="page-header"> | ||
| <h1>Here are all of your characters.</h1> | ||
| </div> | ||
| <div class="row"> | ||
| <div class="col-md-6"> | ||
| <ul id="Character_List"> | ||
| {{if .Character}} {{range .Character}} | ||
| <li> | ||
| <a class="Character" Tag="Character" href="/character/{{ .Name }}"> <strong>{{.Name}}</strong></a> | ||
| </li> | ||
| {{end}} {{else}} | ||
| <p>No results found!</p> | ||
| {{end}} | ||
| </ul> | ||
| </div> | ||
| <div class="col-md-6"> | ||
| <a class="btn btn-success" href="/user/character/new">New</a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {{end}}{{define "scripts"}}{{end}} |
| @@ -5,4 +5,4 @@ | ||
| <h2>{{.Message}}</h2> | ||
| </div> | ||
| </div> | ||
| {{end}} {{define "scripts"}}{{end}} | ||
| @@ -0,0 +1,78 @@ | ||
| {{define "title"}}Login{{end}} {{define "content"}} | ||
| <div class="container"> | ||
| <div class="row"> | ||
| <div class="col-md-6 col-md-offset-3"> | ||
| <div class="panel panel-login"> | ||
| <div class="panel-heading"> | ||
| <div class="row"> | ||
| <div class="col-xs-6"> | ||
| <a href="#" class="active" id="login-form-link">Login</a> | ||
| </div> | ||
| <div class="col-xs-6"> | ||
| <a href="#" id="register-form-link">Register</a> | ||
| </div> | ||
| </div> | ||
| <hr> | ||
| </div> | ||
| <div class="panel-body"> | ||
| <div class="row"> | ||
| <div class="col-lg-12"> | ||
| {{range .Errors}} | ||
| <div class="alert alert-danger"> | ||
| {{.}} | ||
| </div> | ||
| {{end}} | ||
| <form id="login-form" action="/login" method="post" role="form" style="display: block;"> | ||
| <div class="form-group"> | ||
| <input type="text" name="username" id="username" tabindex="1" class="form-control" placeholder="Username" value=""> | ||
| </div> | ||
| <div class="form-group"> | ||
| <input type="password" name="password" id="password" tabindex="2" class="form-control" placeholder="Password"> | ||
| </div> | ||
| <div class="form-group text-center"> | ||
| <input type="checkbox" tabindex="3" class="" name="remember" id="remember"> | ||
| <label for="remember"> Remember Me</label> | ||
| </div> | ||
| <div class="form-group"> | ||
| <div class="row"> | ||
| <div class="col-sm-6 col-sm-offset-3"> | ||
| <input type="submit" name="login-submit" id="login-submit" tabindex="4" class="form-control btn btn-login" value="Log In"> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="form-group"> | ||
| <div class="row"> | ||
| <div class="col-lg-12"> | ||
| <div class="text-center"> | ||
| <a href="/recover" tabindex="5" class="forgot-password">Forgot Password?</a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </form> | ||
| <form id="register-form" action="/register" method="post" role="form" style="display: none;"> | ||
| <div class="form-group"> | ||
| <input type="text" name="username" id="username" tabindex="1" class="form-control" placeholder="Username" value=""> | ||
| </div> | ||
| <div class="form-group"> | ||
| <input type="password" name="password" id="password" tabindex="2" class="form-control" placeholder="Password"> | ||
| </div> | ||
| <div class="form-group"> | ||
| <input type="password" name="confirm-password" id="confirm-password" tabindex="2" class="form-control" placeholder="Confirm Password"> | ||
| </div> | ||
| <div class="form-group"> | ||
| <div class="row"> | ||
| <div class="col-sm-6 col-sm-offset-3"> | ||
| <input type="submit" name="register-submit" id="register-submit" tabindex="4" class="form-control btn btn-register" value="Register Now"> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <link rel="stylesheet" href="/static/css/login.css">{{end}} {{define "scripts" }}<script type="text/javascript" src="/static/js/login.js"></script>{{end}} |
| @@ -0,0 +1,15 @@ | ||
| {{define "title"}}Murder Hobos{{end}} {{define "content"}} | ||
| <br> | ||
| <div class="container"> | ||
| <div class="page-header"> | ||
| <h1>Here's Your Profile, {{.Claims.Username}}</h1> | ||
| </div> | ||
| <div class="row"> | ||
| <div class="col-md-6"> | ||
| <p>We will have some stuff for you here in the future!</p> | ||
| <p>For now, try hovering over your name in the navigation bar to have a look at your spells and characters. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {{end}}{{define "scripts"}}{{end}} |
| @@ -0,0 +1,98 @@ | ||
| {{define "title"}}Create Spell - Murder Hobos{{end}} {{define "content"}} | ||
| <br> | ||
| <div class="page-header"> | ||
| <h1>Spell Creator</h1> | ||
| </div> | ||
| <div> | ||
| <form class="form" method="POST"> | ||
| <div class="form-group" name="nameEntry"> | ||
| <labela>Name: </label> | ||
| <input required type="text" name="name"></input> | ||
| </div> | ||
| <div class="form-group" name="schoolSelection"> | ||
| <label>School: </label> | ||
| <select required class="form-control" name="school"> | ||
| <option selected disabled value="">Select School</option> | ||
| <option value="Abjuration">Abjuration</option> | ||
| <option value="Conjuration">Conjuration</option> | ||
| <option value="Divination">Divination</option> | ||
| <option value="Enchantment">Enchantment</option> | ||
| <option value="Evocation">Evocation</option> | ||
| <option value="Illusion">Illusion</option> | ||
| <option value="Necromancy">Necromancy</option> | ||
| <option value="Transmutation">Transmutation</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group" name="LevelSelection"> | ||
| <label>Level: </label> | ||
| <select required class="form-control" name="level"> | ||
| <option selected disabled value="">Select Level</option> | ||
| <option value="0">Cantrip</option> | ||
| <option value="1">Level 1</option> | ||
| <option value="2">Level 2</option> | ||
| <option value="3">Level 3</option> | ||
| <option value="4">Level 4</option> | ||
| <option value="5">Level 5</option> | ||
| <option value="6">Level 6</option> | ||
| <option value="7">Level 7</option> | ||
| <option value="8">Level 8</option> | ||
| <option value="9">Level 9</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group" name="castTimeEntry"> | ||
| <label>Cast Time: </label> | ||
| <input required type="text" name="castTime" value="castTime"></input> | ||
| </div> | ||
| <div class="form-group" name="durationEntry"> | ||
| <label>Duration: </label> | ||
| <input required type="text" name="duration" value="duration"></input> | ||
| </div> | ||
|
|
||
| <div class="form-group" name="components"> | ||
| <label>Components: </label> | ||
| <label>Verbal</label> | ||
| <select required class="form-control" name="verbal"> | ||
| <option selected disabled value="">---</option> | ||
| <option value="Yes">Yes</option> | ||
| <option value="No">No</option> | ||
| </select> | ||
| <label>Somatic</label> | ||
| <select required class="form-control" name="somatic"> | ||
| <option selected disabled value="">---</option> | ||
| <option value="Yes">Yes</option> | ||
| <option value="No">No</option> | ||
| </select> | ||
| <label>Material</label> | ||
| <select required class="form-control" name="material"> | ||
| <option selected disabled value="">---</option> | ||
| <option value="Yes">Yes</option> | ||
| <option value="No">No</option> | ||
| </select> | ||
| <label>Material Description: </label> | ||
| <input type="text" name="materialDesc" value="Enter Decription..."></input> | ||
| </div> | ||
|
|
||
| <div class="form-group" name="spellType"> | ||
| <label>Spell Requirements: </label> | ||
| <div></div> | ||
| <label>Concentration</label> | ||
| <select required class="form-control" name="concentration"> | ||
| <option selected disabled value="">---</option> | ||
| <option value="Yes">Yes</option> | ||
| <option value="No">No</option> | ||
| </select> | ||
| <label>Ritual</label> | ||
| <select required class="form-control" name="ritual"> | ||
| <option selected disabled value="">---</option> | ||
| <option value="Yes">Yes</option> | ||
| <option value="No">No</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group" name="spellDescript"> | ||
| <label>Spell Decription: </label> | ||
| <input required type="text" name="spellDesc" value="spellDesc"></input> | ||
| </div> | ||
| <input class="btn btn-primary" type="submit" value="Create Spell"></input> | ||
| </form> | ||
| </div> | ||
| {{end}}{{define "scripts"}}{{end}} |
| @@ -0,0 +1,82 @@ | ||
| {{define "title"}}Murder Hobos{{end}} {{define "content"}} | ||
| <br> | ||
| <div class="container"> | ||
| <div class="page-header"> | ||
| <h1>Look at all these spells.</h1> | ||
| </div> | ||
| <div class="row"> | ||
| <div class="col-xs-6 col-sm-6 col-md-4 col-lg-4"> | ||
| <form class="form-inline" method="GET"> | ||
| <div class="form-group"> | ||
| <input class="form-control" type="text" id="spellSearch" name="name" placeholder="Spell name..." title="Search for spells by name"></input> | ||
| <input class="btn btn-primary" type="submit" id="searchButton" value="Search"></input> | ||
| </div> | ||
| </form> | ||
| <form class="form-inline" method="GET"> | ||
| <div class="form-group"> | ||
| <select class="form-control" name="school"> | ||
| <option selected disabled value="">School</option> | ||
| <option value="Abjuration">Abjuration</option> | ||
| <option value="Conjuration">Conjuration</option> | ||
| <option value="Divination">Divination</option> | ||
| <option value="Enchantment">Enchantment</option> | ||
| <option value="Evocation">Evocation</option> | ||
| <option value="Illusion">Illusion</option> | ||
| <option value="Necromancy">Necromancy</option> | ||
| <option value="Transmutation">Transmutation</option> | ||
| </select> | ||
| </div> | ||
| <div class="form-group"> | ||
| <select class="form-control" name="level"> | ||
| <option selected disabled value="">Level</option> | ||
| <option value="0">Cantrip</option> | ||
| <option value="1">Level 1</option> | ||
| <option value="2">Level 2</option> | ||
| <option value="3">Level 3</option> | ||
| <option value="4">Level 4</option> | ||
| <option value="5">Level 5</option> | ||
| <option value="6">Level 6</option> | ||
| <option value="7">Level 7</option> | ||
| <option value="8">Level 8</option> | ||
| <option value="9">Level 9</option> | ||
| </select> | ||
| </div> | ||
| <input class="btn btn-primary" type="submit" value="Filter"></input> | ||
| </form> | ||
| </div> | ||
| {{with .Claims}} | ||
| <div class="col-xs-6 col-sm-6 col-md-4 col-lg-4"> | ||
| <a href="/user/spell/new" class="btn btn-success">New</a> | ||
| </div> | ||
| {{end}} | ||
| </div> | ||
| <br> | ||
| <div class="table-responsive"> | ||
| <table class="table"> | ||
| <thead> | ||
| <tr> | ||
| <th>Name</th> | ||
| <th>School</th> | ||
| <th>Level</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody id="Spell_List"> | ||
| {{if .Spells}} {{range .Spells}} | ||
| <tr> | ||
| <td><a class="Spells" Tag="Spell" href="/user/spell/{{ .Name }}">{{.Name}}</td> | ||
| <td>{{.School}}</td> | ||
| <td>{{.LevelStr}}</td> | ||
| </tr> | ||
| {{end}} {{else}} | ||
| <tr> | ||
| <th>Nothing</th> | ||
| <th>Here</th> | ||
| <th>At The moment</th> | ||
| </tr> | ||
| {{end}} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {{end}}{{define "scripts"}}{{end}} |