diff options
author | Vikas Kushwaha <dev@vikas.rocks> | 2025-02-11 16:31:08 +0530 |
---|---|---|
committer | Vikas Kushwaha <dev@vikas.rocks> | 2025-02-11 16:31:08 +0530 |
commit | 57eb8f6712361a3bf75983ce153fac4846dc0273 (patch) | |
tree | 269a168d59c917c4e313c819e2b4c3ff8175f912 |
Initial commit
91 files changed, 3603 insertions, 0 deletions
diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..e06fbbb --- /dev/null +++ b/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 0 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fe60c2 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +This codebase was created to demonstrate a fully fledged fullstack application built with + + - [Fiber v2](https://github.com/gofiber/fiber/tree/v2.52.6) an Express inspired web framework for Go + - [HTMX](https://htmx.org/) to connect the frontend (html + js) with the backend + - [Slug](https://github.com/gosimple/slug) for user friendly URLS + - [OR Mapper gorm](gorm.io/gorm) and a [Go native driver for GORM to sqlite](https://github.com/glebarez/sqlite) + - and [other packages](go.mod) + +## Project Overview + +"Projecty" is a social project development site for DIY enthusiaists and product builders. + +# Installation +``` +1. clone this repository +2. run: go get +3. this project is using sqlite and its already seeded, look at database.sqlite +4. run: go run main.go +6. use test@email.com|secret for logging in + 6.1. or can register from the web +``` diff --git a/cmd/web/controller/article.go b/cmd/web/controller/article.go new file mode 100644 index 0000000..94735f1 --- /dev/null +++ b/cmd/web/controller/article.go @@ -0,0 +1,63 @@ +package controller + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func ArticleDetailPage(c *fiber.Ctx) error { + + var article model.Article + var authenticatedUser model.User + isSelf := false + isFollowed := false + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User.Followers"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Redirect("/") + } + } + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + if isAuthenticated && article.User.FollowedBy(userID) { + isFollowed = true + } + + if isAuthenticated && article.User.ID == userID { + isSelf = true + } + + return c.Render("articles/show", fiber.Map{ + "PageTitle": article.Title + " — Projecty", + "Article": article, + "FiberCtx": c, + "IsOob": false, + "IsSelf": isSelf, + "IsFollowed": isFollowed, + "IsArticleFavorited": article.FavoritedBy(userID), + "AuthenticatedUser": authenticatedUser, + }, "layouts/app") +} diff --git a/cmd/web/controller/editor.go b/cmd/web/controller/editor.go new file mode 100644 index 0000000..725ac5f --- /dev/null +++ b/cmd/web/controller/editor.go @@ -0,0 +1,51 @@ +package controller + +import ( + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func EditorPage(c *fiber.Ctx) error { + + var authenticatedUser model.User + var article model.Article + hasArticle := false + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return c.Redirect("/") + } + + db := database.Get() + + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + + if c.Params("slug") != "" { + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Find(&article).Error + + if err == nil { + hasArticle = true + } + } + + return c.Render("editor/form", fiber.Map{ + "PageTitle": "Editor — Projecty", + "FiberCtx": c, + "NavBarActive": "editor", + "AuthenticatedUser": authenticatedUser, + "HasArticle": hasArticle, + "Article": article, + }, "layouts/app") +} diff --git a/cmd/web/controller/home.go b/cmd/web/controller/home.go new file mode 100644 index 0000000..42e585e --- /dev/null +++ b/cmd/web/controller/home.go @@ -0,0 +1,79 @@ +package controller + +import ( + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" +) + +func HomePage(c *fiber.Ctx) error { + + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + + if isAuthenticated { + db := database.Get() + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + return c.Render("home/index", fiber.Map{ + "PageTitle": "Home — Projecty", + "FiberCtx": c, + "NavBarActive": "home", + "AuthenticatedUser": authenticatedUser, + "CurrentPage": c.QueryInt("page"), + }, "layouts/app") +} + +func YourFeedPage(c *fiber.Ctx) error { + + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + if isAuthenticated { + db := database.Get() + + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } else { + return c.Redirect("/") + } + + return c.Render("home/index", fiber.Map{ + "PageTitle": "Home — Projecty", + "Personal": true, + "FiberCtx": c, + "NavBarActive": "home", + "AuthenticatedUser": authenticatedUser, + "CurrentPage": c.QueryInt("page"), + }, "layouts/app") +} + +func TagFeedPage(c *fiber.Ctx) error { + + var user model.User + + isAuthenticated, userID := authentication.AuthGet(c) + + if isAuthenticated { + db := database.Get() + db.Model(&model.User{ID: userID}). + First(&user) + } + + return c.Render("home/index", fiber.Map{ + "PageTitle": "Home — Projecty", + "Tag": true, + "TagSlug": c.Params("slug"), + "FiberCtx": c, + "NavBarActive": "home", + "User": user, + "CurrentPage": c.QueryInt("page"), + }, "layouts/app") +} diff --git a/cmd/web/controller/htmx/article-action.go b/cmd/web/controller/htmx/article-action.go new file mode 100644 index 0000000..a7b1f23 --- /dev/null +++ b/cmd/web/controller/htmx/article-action.go @@ -0,0 +1,105 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func ArticleFavoriteAction(c *fiber.Ctx) error { + + var article model.Article + var authenticatedUser model.User + + isArticleFavorited := false + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + + } + + db := database.Get() + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Favorites"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + } + + authenticatedUser.ID = userID + + if article.FavoritedBy(userID) { + db.Model(&article).Association("Favorites").Delete(&authenticatedUser) + } else { + db.Model(&article).Association("Favorites").Append(&authenticatedUser) + isArticleFavorited = true + } + + return c.Render("articles/partials/favorite-button", fiber.Map{ + "Article": article, + "Slug": article.Slug, + "IsArticleFavorited": isArticleFavorited, + "IsOob": true, + }, "layouts/app-htmx") +} + +func ArticleFollowAction(c *fiber.Ctx) error { + + var article model.Article + var authenticatedUser model.User + + isFollowed := false + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + + db := database.Get() + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Favorites"). + Preload("User.Followers"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + } + + authenticatedUser.ID = userID + + if article.User.FollowedBy(userID) { + + f := model.Follow{ + FollowerID: article.UserID, + FollowingID: userID, + } + + db.Model(&article.User).Association("Followers").Find(&f) + db.Delete(&f) + + } else { + db.Model(&article.User).Association("Followers").Append(&model.Follow{FollowerID: article.UserID, FollowingID: userID}) + isFollowed = true + } + + return c.Render("articles/partials/follow-button", fiber.Map{ + "Article": article, + "IsFollowed": isFollowed, + "IsOob": true, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/htmx/article.go b/cmd/web/controller/htmx/article.go new file mode 100644 index 0000000..c8dd8f4 --- /dev/null +++ b/cmd/web/controller/htmx/article.go @@ -0,0 +1,64 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func ArticleDetailPage(c *fiber.Ctx) error { + + var article model.Article + isSelf := false + isFollowed := false + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Redirect("/") + } + } + + if isAuthenticated && article.User.FollowedBy(userID) { + isFollowed = true + } + + if isAuthenticated && article.User.ID == userID { + isSelf = true + } + + return c.Render("articles/htmx-article-page", fiber.Map{ + "PageTitle": article.Title, + "NavBarActive": "none", + "Article": article, + "IsOob": false, + "IsSelf": isSelf, + "IsFollowed": isFollowed, + "IsArticleFavorited": article.FavoritedBy(userID), + "AuthenticatedUser": authenticatedUser, + "FiberCtx": c, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/htmx/comment.go b/cmd/web/controller/htmx/comment.go new file mode 100644 index 0000000..3831c0e --- /dev/null +++ b/cmd/web/controller/htmx/comment.go @@ -0,0 +1,96 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func ArticleDetailCommentList(c *fiber.Ctx) error { + + var article model.Article + + isAuthenticated, _ := authentication.AuthGet(c) + + db := database.Get() + + db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("User"). + Preload("Comments", func(db *gorm.DB) *gorm.DB { + return db.Order("id DESC").Preload("User") + }). + Find(&article) + + return c.Render("articles/partials/comments-wrapper", fiber.Map{ + "Article": article, + "IsAuthenticated": isAuthenticated, + }, "layouts/app-htmx") +} + +func ArticleComment(c *fiber.Ctx) error { + + var ( + errorBag []string + article model.Article + comment model.Comment + authenticatedUser model.User + ) + validate := internal.NewValidator() + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + + db := database.Get() + + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("User"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/", "/htmx/home", c) + } + } + + comment.UserID = userID + comment.ArticleID = article.ID + comment.Body = c.FormValue("comment") + + err = validate.Validate(comment) + if err != nil { + + for _, err := range err.(validator.ValidationErrors) { + errorBag = append(errorBag, internal.ErrorMessage(err.Field(), err.Tag())) + } + + return c.Render("components/error-message", fiber.Map{ + "Errors": errorBag, + }, "layouts/app-htmx") + } + + comment.User = authenticatedUser + + db.Create(&comment) + + return c.Render("articles/htmx-post-comments", fiber.Map{ + "IsOob": true, + "Article": article, + "Comment": comment, + "User": authenticatedUser, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/htmx/editor.go b/cmd/web/controller/htmx/editor.go new file mode 100644 index 0000000..fda61a4 --- /dev/null +++ b/cmd/web/controller/htmx/editor.go @@ -0,0 +1,218 @@ +package HTMXController + +import ( + "encoding/json" + "errors" + "projecty/cmd/web/model" + "projecty/internal" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/gosimple/slug" + "gorm.io/gorm" +) + +func EditorPage(c *fiber.Ctx) error { + + var authenticatedUser model.User + var article model.Article + hasArticle := false + NavBarActive := "editor" + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + if c.Params("slug") != "" { + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Find(&article).Error + + if err == nil { + hasArticle = true + NavBarActive = "none" + } + } + + return c.Render("editor/htmx-editor-page", fiber.Map{ + "PageTitle": "Editor", + "FiberCtx": c, + "NavBarActive": NavBarActive, + "AuthenticatedUser": authenticatedUser, + "HasArticle": hasArticle, + "Article": article, + }, "layouts/app-htmx") +} + +func StoreArticle(c *fiber.Ctx) error { + + type TagItem struct { + Value string + } + + var ( + errorBag []string + tagItems []TagItem + authenticatedUser model.User + ) + validate := internal.NewValidator() + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return c.Redirect("/") + } + + db := database.Get() + + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + + article := &model.Article{ + Title: c.FormValue("title"), + Slug: slug.Make(c.FormValue("title")), + Description: c.FormValue("description"), + Body: c.FormValue("content"), + UserID: userID, + } + + err := validate.Validate(article) + if err != nil { + + for _, err := range err.(validator.ValidationErrors) { + errorBag = append(errorBag, internal.ErrorMessage(err.Field(), err.Tag())) + } + + return c.Render("editor/htmx-editor-page", fiber.Map{ + "IsOob": true, + "FiberCtx": c, + "NavBarActive": "editor", + "Errors": errorBag, + "AuthenticatedUser": authenticatedUser, + }, "layouts/app-htmx") + } + + db.Create(article) + + if c.FormValue("tags") != "" { + json.Unmarshal([]byte(c.FormValue("tags")), &tagItems) + + for i := 0; i < len(tagItems); i++ { + tagItem := tagItems[i] + tag := model.Tag{Name: tagItem.Value} + + err := db.Model(&tag).Where("name = ?", tagItem.Value).First(&tag).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + db.Create(&tag) + } + + if err := db.Model(&article).Association("Tags").Append(&tag); err != nil { + return err + } + } + } + + return helper.HTMXRedirectTo("/articles/"+article.Slug, "/htmx/articles/"+article.Slug, c) +} + +func UpdateArticle(c *fiber.Ctx) error { + + type TagItem struct { + Value string + } + + var ( + errorBag []string + tagItems []TagItem + authenticatedUser model.User + article model.Article + tags []model.Tag + ) + + validate := internal.NewValidator() + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return c.Redirect("/") + } + + db := database.Get() + + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Redirect("/") + } + } + + article.Title = c.FormValue("title") + article.Description = c.FormValue("description") + article.Body = c.FormValue("content") + + err = validate.Validate(article) + if err != nil { + + for _, err := range err.(validator.ValidationErrors) { + errorBag = append(errorBag, internal.ErrorMessage(err.Field(), err.Tag())) + } + + return c.Render("editor/htmx-editor-page", fiber.Map{ + "IsOob": true, + "FiberCtx": c, + "NavBarActive": "editor", + "Errors": errorBag, + "AuthenticatedUser": authenticatedUser, + "HasArticle": true, + "Article": article, + }, "layouts/app-htmx") + } + + article.Slug = slug.Make(c.FormValue("title")) + + db.Updates(article) + + if c.FormValue("tags") != "" { + json.Unmarshal([]byte(c.FormValue("tags")), &tagItems) + + for i := 0; i < len(tagItems); i++ { + tagItem := tagItems[i] + tag := model.Tag{Name: tagItem.Value} + + err := db.Model(&tag).Where("name = ?", tagItem.Value).First(&tag).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + db.Create(&tag) + } + + tags = append(tags, tag) + } + + if err := db.Model(&article).Association("Tags").Replace(&tags); err != nil { + return err + } + } + + return helper.HTMXRedirectTo("/articles/"+article.Slug, "/htmx/articles/"+article.Slug, c) +} diff --git a/cmd/web/controller/htmx/home-action.go b/cmd/web/controller/htmx/home-action.go new file mode 100644 index 0000000..5c4b872 --- /dev/null +++ b/cmd/web/controller/htmx/home-action.go @@ -0,0 +1,53 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func HomeFavoriteAction(c *fiber.Ctx) error { + + var article model.Article + var authenticatedUser model.User + + isArticleFavorited := false + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + + db := database.Get() + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Favorites"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + } + + authenticatedUser.ID = userID + + if article.FavoritedBy(userID) { + db.Model(&article).Association("Favorites").Delete(&authenticatedUser) + } else { + db.Model(&article).Association("Favorites").Append(&authenticatedUser) + isArticleFavorited = true + } + + return c.Render("home/partials/article-favorite-button", fiber.Map{ + "GetFavoriteCount": article.GetFavoriteCount(), + "Slug": article.Slug, + "IsFavorited": isArticleFavorited, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/htmx/home.go b/cmd/web/controller/htmx/home.go new file mode 100644 index 0000000..07378d4 --- /dev/null +++ b/cmd/web/controller/htmx/home.go @@ -0,0 +1,322 @@ +package HTMXController + +import ( + "math" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func HomePage(c *fiber.Ctx) error { + + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + + if isAuthenticated { + db := database.Get() + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + return c.Render("home/htmx-home-page", fiber.Map{ + "PageTitle": "Home", + "NavBarActive": "home", + "FiberCtx": c, + "AuthenticatedUser": authenticatedUser, + }, "layouts/app-htmx") +} + +func HomeYourFeed(c *fiber.Ctx) error { + var ( + articles []model.Article + hasArticles bool + user model.User + followings []model.Follow + hasPagination bool + totalPagination int + count int64 + ) + + page := 0 + if c.QueryInt("page") > 1 { + page = c.QueryInt("page") - 1 + } + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return c.Redirect("/") + } + + db := database.Get() + db.Model(&user).Where("id = ?", userID).First(&user) + + db.Model(&user).Preload("Followings").Association("Followings").Find(&followings) + if len(followings) == 0 { + hasArticles = false + } + + ids := make([]uint, len(followings)) + for i, f := range followings { + ids[i] = f.FollowerID + } + + db.Where("user_id in (?)", ids). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User"). + Limit(5). + Offset(page * 5). + Order("created_at desc"). + Find(&articles) + + db.Model(&articles).Where("user_id in (?)", ids).Count(&count) + + if count > 0 && (count/5 > 0) { + pageDivision := float64(count) / float64(5) + totalPagination = int(math.Ceil(pageDivision)) + hasPagination = true + } + + feedNavbarItems := []fiber.Map{ + { + "Title": "Your Feed", + "IsActive": true, + "HXPushURL": "/your-feed", + "HXGetURL": "/htmx/home/your-feed", + }, + { + "Title": "Global Feed", + "IsActive": false, + "HXPushURL": "/", + "HXGetURL": "/htmx/home/global-feed", + }, + } + + if len(articles) > 0 { + hasArticles = true + } + + c.Render("home/htmx-home-feed", fiber.Map{ + "HasArticles": hasArticles, + "Articles": articles, + "FeedNavbarItems": feedNavbarItems, + "Personal": isAuthenticated, + "TotalPagination": totalPagination, + "HasPagination": hasPagination, + "CurrentPagination": page + 1, + "PushPathPagination": "your-feed", + "PathPagination": "your-feed", + }, "layouts/app-htmx") + + return nil +} + +func HomeGlobalFeed(c *fiber.Ctx) error { + + var ( + articles []model.Article + hasArticles bool + hasPagination bool + totalPagination int + count int64 + ) + + page := 0 + if c.QueryInt("page") > 1 { + page = c.QueryInt("page") - 1 + } + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + db.Model(&articles). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User"). + Limit(5). + Offset(page * 5). + Order("created_at desc"). + Find(&articles) + + db.Model(&articles).Count(&count) + + feedNavbarItems := []fiber.Map{ + { + "Title": "Global Feed", + "IsActive": true, + "HXPushURL": "/", + "HXGetURL": "/htmx/home/global-feed", + }, + } + + if count > 0 && (count/5 > 0) { + pageDivision := float64(count) / float64(5) + totalPagination = int(math.Ceil(pageDivision)) + hasPagination = true + } + + if isAuthenticated { + + feedNavbarItems = append([]fiber.Map{ + { + "Title": "Your Feed", + "IsActive": false, + "HXPushURL": "/your-feed", + "HXGetURL": "/htmx/home/your-feed", + }, + }, feedNavbarItems...) + } + + if len(articles) > 0 { + hasArticles = true + + for i := 0; i < len(articles); i++ { + articles[i].IsFavorited = articles[i].FavoritedBy(userID) + } + } + + c.Render("home/htmx-home-feed", fiber.Map{ + "HasArticles": hasArticles, + "Articles": articles, + "FeedNavbarItems": feedNavbarItems, + "AuthenticatedUserID": userID, + "TotalPagination": totalPagination, + "HasPagination": hasPagination, + "CurrentPagination": page + 1, + "PathPagination": "global-feed", + }, "layouts/app-htmx") + + return nil +} + +func HomeTagFeed(c *fiber.Ctx) error { + + var ( + tag model.Tag + articles []model.Article + hasArticles bool + hasPagination bool + totalPagination int + count int64 + ) + + page := 0 + if c.QueryInt("page") > 1 { + page = c.QueryInt("page") - 1 + } + + isAuthenticated, _ := authentication.AuthGet(c) + + tagText := c.Params("tag") + + db := database.Get() + + db.Where(&model.Tag{Name: tagText}).First(&tag) + + db.Model(&tag). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User"). + Limit(5). + Offset(page * 5). + Order("created_at desc"). + Association("Articles"). + Find(&articles) + + count = db.Model(&tag). + Association("Articles"). + Count() + + if len(articles) > 0 { + hasArticles = true + } + + if count > 0 && (count/5 > 0) { + pageDivision := float64(count) / float64(5) + totalPagination = int(math.Ceil(pageDivision)) + hasPagination = true + } + + feedNavbarItems := []fiber.Map{ + { + "Title": "Global Feed", + "IsActive": false, + "HXPushURL": "/", + "HXGetURL": "/htmx/home/global-feed", + }, + } + + if isAuthenticated { + + feedNavbarItems = append([]fiber.Map{ + { + "Title": "Your Feed", + "IsActive": false, + "HXPushURL": "/your-feed", + "HXGetURL": "/htmx/home/your-feed", + }, + }, feedNavbarItems...) + } + + feedNavbarItems = append(feedNavbarItems, + fiber.Map{ + "Title": tagText, + "IsActive": true, + "HXPushURL": "/", + "HXGetURL": "/htmx/home/global-feed", + }, + ) + + c.Render("home/htmx-home-feed", fiber.Map{ + "HasArticles": hasArticles, + "Articles": articles, + "FeedNavbarItems": feedNavbarItems, + "TotalPagination": totalPagination, + "HasPagination": hasPagination, + "CurrentPagination": page + 1, + "PushPathPagination": "tag-feed/" + tag.Name, + "PathPagination": "tag-feed/" + tag.Name, + }, "layouts/app-htmx") + + return nil +} + +func HomeTagList(c *fiber.Ctx) error { + + var ( + tag model.Tag + tags []model.Tag + hasTags bool + ) + + db := database.Get() + db.Model(&tag). + Select("*, COUNT(id) as favorite_count"). + Preload("Articles"). + Limit(5). + Order("favorite_count DESC"). + Group("id"). + Find(&tags) + + if len(tags) > 0 { + hasTags = true + } + + c.Render("home/partials/tag-item-list", fiber.Map{ + "Tags": tags, + "HasTags": hasTags, + }, "layouts/app-htmx") + + return nil +} diff --git a/cmd/web/controller/htmx/setting.go b/cmd/web/controller/htmx/setting.go new file mode 100644 index 0000000..28c138c --- /dev/null +++ b/cmd/web/controller/htmx/setting.go @@ -0,0 +1,81 @@ +package HTMXController + +import ( + "projecty/cmd/web/model" + "projecty/internal" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +func SettingPage(c *fiber.Ctx) error { + + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + + if isAuthenticated { + db := database.Get() + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + return c.Render("settings/htmx-setting-page", fiber.Map{ + "PageTitle": "Settings", + "NavBarActive": "settings", + "FiberCtx": c, + "AuthenticatedUser": authenticatedUser, + }, "layouts/app-htmx") + +} + +func SettingAction(c *fiber.Ctx) error { + + var errorBag []string + validate := internal.NewValidator() + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + + user := &model.User{ + ID: userID, + Image: c.FormValue("image"), + Name: c.FormValue("name"), + Bio: c.FormValue("bio"), + Email: c.FormValue("email"), + Password: c.FormValue("password"), + } + + err := validate.Validate(user) + if err != nil { + for _, err := range err.(validator.ValidationErrors) { + errorBag = append(errorBag, internal.ErrorMessage(err.Field(), err.Tag())) + } + + return c.Render("settings/partials/form-message", fiber.Map{ + "IsOob": true, + "Errors": errorBag, + }, "layouts/app-htmx") + } + + if user.Password != "" { + user.HashPassword() + } + + db := database.Get() + db.Model(user).Updates(user) + + return c.Render("settings/partials/htmx-form-message", fiber.Map{ + "IsOob": true, + "SuccessMessages": []string{"Data successfully saved."}, + "NavBarActive": "settings", + "FiberCtx": c, + "AuthenticatedUser": user, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/htmx/sign-in.go b/cmd/web/controller/htmx/sign-in.go new file mode 100644 index 0000000..df60027 --- /dev/null +++ b/cmd/web/controller/htmx/sign-in.go @@ -0,0 +1,80 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func SignInPage(c *fiber.Ctx) error { + + return c.Render("sign-in/htmx-sign-in-page", fiber.Map{ + "PageTitle": "Sign In", + "NavBarActive": "sign-in", + "FiberCtx": c, + }, "layouts/app-htmx") + +} + +func SignInAction(c *fiber.Ctx) error { + + var user model.User + email := c.FormValue("email") + password := c.FormValue("password") + + if email == "" || password == "" { + + return c.Render("sign-in/partials/sign-in-form", fiber.Map{ + "Errors": []string{ + "Email or password cannot be null.", + }, + "IsOob": true, + }, "layouts/app-htmx") + } + + db := database.Get() + + db.Model(&user) + err := db.Where(&model.User{Email: email}). + First(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Render("sign-in/partials/sign-in-form", fiber.Map{ + "Errors": []string{ + "Email and password did not match.", + }, + }, "layouts/app-htmx") + } + } + + if !user.CheckPassword(password) { + + return c.Render("sign-in/partials/sign-in-form", fiber.Map{ + "Errors": []string{ + "Email and password did not match.", + }, + }, "layouts/app-htmx") + } + + authentication.AuthStore(c, user.ID) + + return helper.HTMXRedirectTo("/", "/htmx/home", c) +} + +func SignOut(c *fiber.Ctx) error { + + isAuthenticated, _ := authentication.AuthGet(c) + if !isAuthenticated { + return c.Redirect("/") + } + + authentication.AuthDestroy(c) + + return helper.HTMXRedirectTo("/", "/htmx/home", c) +} diff --git a/cmd/web/controller/htmx/sign-up.go b/cmd/web/controller/htmx/sign-up.go new file mode 100644 index 0000000..6bf9a3f --- /dev/null +++ b/cmd/web/controller/htmx/sign-up.go @@ -0,0 +1,46 @@ +package HTMXController + +import ( + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/gofiber/fiber/v2" +) + +func SignUpPage(c *fiber.Ctx) error { + + return c.Render("sign-up/htmx-sign-up-page", fiber.Map{ + "PageTitle": "Sign Up", + "NavBarActive": "sign-up", + "FiberCtx": c, + }, "layouts/app-htmx") +} + +func SignUpAction(c *fiber.Ctx) error { + + username := c.FormValue("username") + email := c.FormValue("email") + password := c.FormValue("password") + + if email == "" || username == "" || password == "" { + + return c.Render("sign-up/partials/sign-up-form", fiber.Map{ + "Errors": []string{ + "Username, email, and password cannot be null.", + }, + "IsOob": true, + }, "layouts/app-htmx") + } + + user := model.User{Username: username, Email: email, Password: password, Name: username} + user.HashPassword() + + db := database.Get() + db.Create(&user) + + authentication.AuthStore(c, user.ID) + + return helper.HTMXRedirectTo("/", "/htmx/home", c) +} diff --git a/cmd/web/controller/htmx/user-action.go b/cmd/web/controller/htmx/user-action.go new file mode 100644 index 0000000..507bfec --- /dev/null +++ b/cmd/web/controller/htmx/user-action.go @@ -0,0 +1,96 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func UserArticleFavoriteAction(c *fiber.Ctx) error { + + var article model.Article + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + + db := database.Get() + + err := db.Model(&article). + Where("slug = ?", c.Params("slug")). + Preload("Favorites"). + Find(&article).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/sign-in", "/htmx/sign-in", c) + } + } + + authenticatedUser.ID = userID + + if article.FavoritedBy(userID) { + db.Model(&article).Association("Favorites").Delete(&authenticatedUser) + article.IsFavorited = false + } else { + db.Model(&article).Association("Favorites").Append(&authenticatedUser) + article.IsFavorited = true + } + + return c.Render("users/partials/article-favorite-button", fiber.Map{ + "Article": article, + }, "layouts/app-htmx") +} + +func UserFollowAction(c *fiber.Ctx) error { + + var authenticatedUser model.User + var user model.User + isFollowed := false + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Model(&user). + Where("username = ?", c.Params("username")). + Preload("Followers"). + Find(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/", "/htmx/home", c) + } + } + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + f := model.Follow{ + FollowerID: user.ID, + FollowingID: userID, + } + + if user.FollowedBy(userID) { + db.Model(&user).Association("Followers").Find(&f) + db.Delete(&f) + } else { + db.Model(&user).Association("Followers").Append(&f) + isFollowed = true + } + + return c.Render("users/partials/follow-button", fiber.Map{ + "User": user, + "IsFollowed": isFollowed, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/htmx/user.go b/cmd/web/controller/htmx/user.go new file mode 100644 index 0000000..b0a2605 --- /dev/null +++ b/cmd/web/controller/htmx/user.go @@ -0,0 +1,195 @@ +package HTMXController + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/helper" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func UserDetailPage(c *fiber.Ctx) error { + + var authenticatedUser model.User + var user model.User + isSelf := false + isFollowed := false + navbarActive := "none" + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Model(&user). + Where("username = ?", c.Params("username")). + Preload("Followers"). + Find(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/", "/htmx/home", c) + } + } + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + if isAuthenticated && user.ID == userID { + isSelf = true + navbarActive = "profile" + } + + if isAuthenticated && !isSelf && user.FollowedBy(userID) { + isFollowed = true + } + + return c.Render("users/htmx-users-page", fiber.Map{ + "PageTitle": user.Name, + "IsSelf": isSelf, + "IsFollowed": isFollowed, + "AuthenticatedUser": authenticatedUser, + "User": user, + "NavBarActive": navbarActive, + "FiberCtx": c, + }, "layouts/app-htmx") +} + +func UserArticles(c *fiber.Ctx) error { + + var articles []model.Article + var user model.User + hasArticles := false + + _, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Where(&user). + Where("username = ?", c.Params("username")). + First(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/", "/htmx/home", c) + } + } + + db.Where(&model.Article{UserID: user.ID}). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User"). + Order("created_at desc"). + Find(&articles) + + if len(articles) > 0 { + hasArticles = true + + for i := 0; i < len(articles); i++ { + articles[i].IsFavorited = articles[i].FavoritedBy(userID) + } + } + + feedNavbarItems := []fiber.Map{ + { + "Title": "Articles", + "IsActive": true, + "HXPushURL": "/users/" + user.Username, + "HXGetURL": "/htmx/users/" + user.Username, + }, + { + "Title": "Favorited Articles", + "IsActive": false, + "HXPushURL": "/users/" + user.Username + "/favorites", + "HXGetURL": "/htmx/users/" + user.Username + "/favorites", + }, + } + + return c.Render("users/htmx-users-articles", fiber.Map{ + "HasArticles": hasArticles, + "Articles": articles, + "User": user, + "FeedNavbarItems": feedNavbarItems, + }, "layouts/app-htmx") +} + +func UserArticlesFavorite(c *fiber.Ctx) error { + + var articles []model.Article + var user model.User + isSelf := false + isFollowed := false + hasArticles := false + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Model(&user). + Where("username = ?", c.Params("username")). + First(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return helper.HTMXRedirectTo("/", "/htmx/home", c) + } + } + + db.Model(&user). + Preload("Favorites"). + Preload("Tags", func(db *gorm.DB) *gorm.DB { + return db.Order("tags.name asc") + }). + Preload("User"). + Order("created_at desc"). + Association("Favorites"). + Find(&articles) + + if len(articles) > 0 { + hasArticles = true + + for i := 0; i < len(articles); i++ { + articles[i].IsFavorited = articles[i].FavoritedBy(userID) + } + } + + if isAuthenticated && user.ID == userID { + isSelf = true + } + + if isAuthenticated && !isSelf && user.FollowedBy(userID) { + isFollowed = true + } + + feedNavbarItems := []fiber.Map{ + { + "Title": "Articles", + "IsActive": false, + "HXPushURL": "/users/" + user.Username + "/articles", + "HXGetURL": "/htmx/users/" + user.Username + "/articles", + }, + { + "Title": "Favorited Articles", + "IsActive": true, + "HXPushURL": "/users/" + user.Username + "/favorites", + "HXGetURL": "/htmx/users/" + user.Username + "/favorites", + }, + } + + return c.Render("users/htmx-users-articles", fiber.Map{ + "IsSelf": isSelf, + "IsFollowed": isFollowed, + "HasArticles": hasArticles, + "Articles": articles, + "User": user, + "FeedNavbarItems": feedNavbarItems, + "IsLoadFavorites": true, + }, "layouts/app-htmx") +} diff --git a/cmd/web/controller/setting.go b/cmd/web/controller/setting.go new file mode 100644 index 0000000..fb6e96e --- /dev/null +++ b/cmd/web/controller/setting.go @@ -0,0 +1,32 @@ +package controller + +import ( + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" +) + +func SettingPage(c *fiber.Ctx) error { + + var authenticatedUser model.User + + isAuthenticated, userID := authentication.AuthGet(c) + if !isAuthenticated { + return c.Redirect("/") + } + + db := database.Get() + + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + + return c.Render("settings/index", fiber.Map{ + "PageTitle": "Settings — Projecty", + "FiberCtx": c, + "NavBarActive": "settings", + "AuthenticatedUser": authenticatedUser, + }, "layouts/app") +} diff --git a/cmd/web/controller/sign-in.go b/cmd/web/controller/sign-in.go new file mode 100644 index 0000000..e73d0c4 --- /dev/null +++ b/cmd/web/controller/sign-in.go @@ -0,0 +1,21 @@ +package controller + +import ( + "projecty/internal/authentication" + + "github.com/gofiber/fiber/v2" +) + +func SignInPage(c *fiber.Ctx) error { + + isAuthenticated, _ := authentication.AuthGet(c) + if isAuthenticated { + return c.Redirect("/") + } + + return c.Render("sign-in/index", fiber.Map{ + "PageTitle": "Sign In — Projecty", + "FiberCtx": c, + "NavBarActive": "sign-in", + }, "layouts/app") +} diff --git a/cmd/web/controller/sign-up.go b/cmd/web/controller/sign-up.go new file mode 100644 index 0000000..3549009 --- /dev/null +++ b/cmd/web/controller/sign-up.go @@ -0,0 +1,21 @@ +package controller + +import ( + "projecty/internal/authentication" + + "github.com/gofiber/fiber/v2" +) + +func SignUpPage(c *fiber.Ctx) error { + + isAuthenticated, _ := authentication.AuthGet(c) + if isAuthenticated { + return c.Redirect("/") + } + + return c.Render("sign-up/index", fiber.Map{ + "PageTitle": "Sign Up — Projecty", + "FiberCtx": c, + "NavBarActive": "sign-up", + }, "layouts/app") +} diff --git a/cmd/web/controller/user.go b/cmd/web/controller/user.go new file mode 100644 index 0000000..3b3bd07 --- /dev/null +++ b/cmd/web/controller/user.go @@ -0,0 +1,110 @@ +package controller + +import ( + "errors" + "projecty/cmd/web/model" + "projecty/internal/authentication" + "projecty/internal/database" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func UserDetailPage(c *fiber.Ctx) error { + + var user model.User + var authenticatedUser model.User + isSelf := false + isFollowed := false + navbarActive := "none" + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Model(&user). + Where("username = ?", c.Params("username")). + Preload("Followers"). + Find(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Redirect("/") + } + } + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + if isAuthenticated && user.ID == userID { + isSelf = true + navbarActive = "profile" + } + + if isAuthenticated && !isSelf && user.FollowedBy(userID) { + isFollowed = true + } + + return c.Render("users/show", fiber.Map{ + "PageTitle": user.Name + " — Projecty", + "FiberCtx": c, + "IsSelf": isSelf, + "IsFollowed": isFollowed, + "User": user, + "AuthenticatedUser": authenticatedUser, + "NavBarActive": navbarActive, + }, "layouts/app") +} + +func UserDetailFavoritePage(c *fiber.Ctx) error { + + var user model.User + var authenticatedUser model.User + isSelf := false + isFollowed := false + navbarActive := "none" + + isAuthenticated, userID := authentication.AuthGet(c) + + db := database.Get() + + err := db.Model(&user). + Where("username = ?", c.Params("username")). + Preload("Followers"). + Find(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Redirect("/") + } + } + + if isAuthenticated { + db.Model(&authenticatedUser). + Where("id = ?", userID). + First(&authenticatedUser) + } + + if isAuthenticated && user.ID == userID { + isSelf = true + navbarActive = "profile" + } + + if isAuthenticated && !isSelf && user.FollowedBy(userID) { + isFollowed = true + } + + return c.Render("users/show", fiber.Map{ + "PageTitle": user.Name + " — Projecty", + "FiberCtx": c, + "IsSelf": isSelf, + "IsFollowed": isFollowed, + "User": user, + "AuthenticatedUser": authenticatedUser, + "NavBarActive": navbarActive, + "IsLoadFavorites": true, + }, "layouts/app") +} diff --git a/cmd/web/model/article.go b/cmd/web/model/article.go new file mode 100644 index 0000000..1475a0c --- /dev/null +++ b/cmd/web/model/article.go @@ -0,0 +1,52 @@ +package model + +import ( + "gorm.io/gorm" +) + +type Article struct { + gorm.Model + Slug string `gorm:"uniqueIndex;not null"` + Title string `gorm:"not null" validate:"required"` + Description string `validate:"required"` + Body string `validate:"required"` + User User `validate:"-"` + UserID uint + Comments []Comment + Favorites []User `gorm:"many2many:article_favorite;"` + Tags []Tag `gorm:"many2many:article_tag;"` + IsFavorited bool `gorm:"-"` +} + +func (Article Article) GetFormattedCreatedAt() string { + dateLayout := "Jan 02, 2006" + return Article.CreatedAt.Format(dateLayout) +} + +func (Article Article) GetFavoriteCount() int { + return len(Article.Favorites) +} + +func (Article Article) FavoritedBy(id uint) bool { + if Article.Favorites == nil { + return false + } + + for _, u := range Article.Favorites { + if u.ID == id { + return true + } + } + + return false +} + +func (Article Article) GetTagsAsCommaSeparated() string { + tagsText := "" + + for i := 0; i < len(Article.Tags); i++ { + tagsText += Article.Tags[i].Name + "," + } + + return tagsText +} diff --git a/cmd/web/model/comment.go b/cmd/web/model/comment.go new file mode 100644 index 0000000..c979edc --- /dev/null +++ b/cmd/web/model/comment.go @@ -0,0 +1,17 @@ +package model + +import "gorm.io/gorm" + +type Comment struct { + gorm.Model + Article Article `validate:"-"` + ArticleID uint + User User `validate:"-"` + UserID uint + Body string `validate:"required"` +} + +func (Comment Comment) GetFormattedCreatedAt() string { + dateLayout := "Jan 02, 2006" + return Comment.CreatedAt.Format(dateLayout) +} diff --git a/cmd/web/model/tag.go b/cmd/web/model/tag.go new file mode 100644 index 0000000..db38a07 --- /dev/null +++ b/cmd/web/model/tag.go @@ -0,0 +1,9 @@ +package model + +import "gorm.io/gorm" + +type Tag struct { + gorm.Model + Name string `gorm:"uniqueIndex"` + Articles []Article `gorm:"many2many:article_tag;"` +} diff --git a/cmd/web/model/user.go b/cmd/web/model/user.go new file mode 100644 index 0000000..6a5a388 --- /dev/null +++ b/cmd/web/model/user.go @@ -0,0 +1,59 @@ +package model + +import ( + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type User struct { + gorm.Model + ID uint + Name string `validate:"required"` + Username string `gorm:"uniqueIndex;not nul"` + Email string `gorm:"uniqueIndex;not null" validate:"required,email"` + Password string `gorm:"not null"` + Bio string + Image string + Followers []Follow `gorm:"foreignKey:FollowerID"` + Followings []Follow `gorm:"foreignKey:FollowingID"` + Favorites []Article `gorm:"many2many:article_favorite;"` +} + +type Follow struct { + Follower User + FollowerID uint `gorm:"column:user_id;primaryKey" sql:"type:int not null"` + Following User + FollowingID uint `gorm:"column:follower_id;primaryKey" sql:"type:int not null"` +} + +func (u *User) HashPassword() { + h, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + u.Password = string(h) +} + +func (u *User) CheckPassword(plain string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(plain)) + return err == nil +} + +func (u User) FollowedBy(id uint) bool { + if u.Followers == nil { + return false + } + + for _, f := range u.Followers { + if f.FollowingID == id { + return true + } + } + + return false +} + +func (u User) FollowersCount() int { + return len(u.Followers) +} + +func (Follow) TableName() string { + return "user_follower" +} diff --git a/cmd/web/route/handlers.go b/cmd/web/route/handlers.go new file mode 100644 index 0000000..0131fe8 --- /dev/null +++ b/cmd/web/route/handlers.go @@ -0,0 +1,39 @@ +package webroute + +import ( + "projecty/cmd/web/controller" + + "github.com/gofiber/fiber/v2" +) + +type PageData struct { + PageTitle string +} + +func WebHandlers(app *fiber.App) { + + /* Sign In */ + app.Get("/sign-in", controller.SignInPage) + + /* Sign Up */ + app.Get("/sign-up", controller.SignUpPage) + + /* Home */ + app.Get("/", controller.HomePage) + app.Get("/your-feed", controller.YourFeedPage) + app.Get("/tag-feed/:slug", controller.TagFeedPage) + + /* Article */ + app.Get("/articles/:slug", controller.ArticleDetailPage) + + /* Editor */ + app.Get("/editor/:slug?", controller.EditorPage) + + /* User */ + app.Get("/users/:username", controller.UserDetailPage) + app.Get("/users/:username/articles", controller.UserDetailPage) + app.Get("/users/:username/favorites", controller.UserDetailFavoritePage) + + /* Setting */ + app.Get("/settings", controller.SettingPage) +} diff --git a/cmd/web/route/htmx-handlers.go b/cmd/web/route/htmx-handlers.go new file mode 100644 index 0000000..4470c91 --- /dev/null +++ b/cmd/web/route/htmx-handlers.go @@ -0,0 +1,50 @@ +package webroute + +import ( + HTMXController "projecty/cmd/web/controller/htmx" + + "github.com/gofiber/fiber/v2" +) + +func HTMXHandlers(app *fiber.App) { + + /* Sign In */ + app.Get("/htmx/sign-in", HTMXController.SignInPage) + app.Post("/htmx/sign-in", HTMXController.SignInAction) + app.Post("/htmx/sign-out", HTMXController.SignOut) + + /* Sign Up */ + app.Get("/htmx/sign-up", HTMXController.SignUpPage) + app.Post("/htmx/sign-up", HTMXController.SignUpAction) + + /* Home */ + app.Get("/htmx/home", HTMXController.HomePage) + app.Get("/htmx/home/your-feed", HTMXController.HomeYourFeed) + app.Get("/htmx/home/global-feed", HTMXController.HomeGlobalFeed) + app.Get("/htmx/home/tag-feed/:tag", HTMXController.HomeTagFeed) + app.Get("/htmx/home/tag-list", HTMXController.HomeTagList) + app.Post("/htmx/home/articles/:slug/favorite", HTMXController.HomeFavoriteAction) + + /* Article */ + app.Get("/htmx/articles/:slug", HTMXController.ArticleDetailPage) + app.Get("/htmx/articles/:slug/comments", HTMXController.ArticleDetailCommentList) + app.Post("/htmx/articles/:slug/comments", HTMXController.ArticleComment) + app.Post("/htmx/articles/:slug/favorite", HTMXController.ArticleFavoriteAction) + app.Post("/htmx/articles/follow-user/:slug", HTMXController.ArticleFollowAction) + + /* Editor */ + app.Get("/htmx/editor/:slug?", HTMXController.EditorPage) + app.Post("/htmx/editor", HTMXController.StoreArticle) + app.Patch("/htmx/editor/:slug?", HTMXController.UpdateArticle) + + /* User */ + app.Get("/htmx/users/:username", HTMXController.UserDetailPage) + app.Get("/htmx/users/:username/articles", HTMXController.UserArticles) + app.Get("/htmx/users/:username/favorites", HTMXController.UserArticlesFavorite) + app.Post("/htmx/users/articles/:slug/favorite", HTMXController.UserArticleFavoriteAction) + app.Post("/htmx/users/:username/follow", HTMXController.UserFollowAction) + + /* Setting */ + app.Get("/htmx/settings", HTMXController.SettingPage) + app.Post("/htmx/settings", HTMXController.SettingAction) +} diff --git a/cmd/web/serve.go b/cmd/web/serve.go new file mode 100644 index 0000000..290987d --- /dev/null +++ b/cmd/web/serve.go @@ -0,0 +1,21 @@ +package web + +import ( + webroute "projecty/cmd/web/route" + "projecty/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +var appName = "Projecty" + +func Serve(app *fiber.App) { + + app.Static("/static", "./cmd/web/static") + + webroute.WebHandlers(app) + webroute.HTMXHandlers(app) + + // Handle not founds + app.Use(middleware.NotFound) +} diff --git a/cmd/web/static/css/style.css b/cmd/web/static/css/style.css new file mode 100644 index 0000000..7a88db7 --- /dev/null +++ b/cmd/web/static/css/style.css @@ -0,0 +1,5 @@ +.logo-font,.navbar-brand{font-family:"Titillium Web",sans-serif}.banner h1,.home-page .banner h1{text-shadow:0 1px 3px rgba(0,0,0,.3)}pre,textarea{overflow:auto}html{position:relative;min-height:100vh;padding-bottom:100px}.navbar-brand{font-size:1.5rem!important;padding-top:0!important;margin-right:2rem!important;color:#5CB85C!important}.nav-link:hover{transition:.1s all}.nav-pills.outline-active .nav-link{border-radius:0;border:none;border-bottom:2px solid transparent;background:0 0;color:#aaa}.nav-pills.outline-active .nav-link:hover{color:#555}.nav-pills.outline-active .nav-link.active{background:#fff!important;border-bottom:2px solid #5CB85C!important;color:#5CB85C!important}footer{background:#f3f3f3;margin-top:3rem;padding:1rem 0;position:absolute;bottom:0;width:100%}.checkbox,.post-meta,.radio,sub,sup{position:relative}footer .attribution{vertical-align:top;margin-left:10px;font-size:.8rem;color:#bbb;font-weight:300}.error-messages{color:#B85C5C!important;font-weight:700}.banner{color:#fff;background:#333;padding:2rem;margin-bottom:2rem}.banner h1{margin-bottom:0}.container.page{margin-top:1.5rem}.preview-link{color:inherit!important}.preview-link:hover{text-decoration:inherit!important}.post-meta{display:block;font-weight:300}.post-meta .info,.post-meta img{display:inline-block;vertical-align:middle}.post-meta img{height:32px;width:32px;border-radius:30px}.post-meta .info{margin:0 1.5rem 0 .3rem;line-height:1rem}.post-meta .info .author{display:block;font-weight:500!important}.post-meta .info .date{color:#bbb;font-size:.8rem;display:block}.post-preview{border-top:1px solid rgba(0,0,0,.1);padding:1.5rem 0}.post-preview .post-meta{margin:0 0 1rem}.post-preview .preview-link h1{font-weight:700!important;font-size:2rem!important}.post-preview .preview-link p{font-family:'Source Serif Pro',serif;margin-bottom:0}.post-preview .preview-link span{font-size:.8rem;font-weight:300;color:#bbb}.btn .counter{font-size:.8rem!important}.home-page .banner{background:#5CB85C;box-shadow:inset 0 8px 8px -8px rgba(0,0,0,.3),inset 0 -8px 8px -8px rgba(0,0,0,.3)}.home-page .banner p{color:#fff;text-align:center;font-size:1.5rem;font-weight:300!important;margin-bottom:0}.home-page .banner h1{font-weight:700!important;text-align:center;font-size:3.5rem;padding-bottom:.5rem}.home-page .feed-toggle{margin-bottom:-1px}.home-page .sidebar{padding:5px 10px 10px;background:#f3f3f3;border-radius:4px}.home-page .sidebar p{margin-bottom:.2rem}.post-page .banner{padding:3rem 0 2rem}.post-page .banner h1{font-size:2.8rem}.post-page .banner .btn{opacity:.8}.post-page .banner .btn:hover{transition:.1s all;opacity:1}.post-page .banner .post-meta{margin:2rem 0 0}.post-page .banner .post-meta .author{color:#fff}.post-page .post-content p{font-family:'Source Serif Pro',serif;font-size:1.2rem;line-height:1.8rem;margin-bottom:2rem}.post-page .post-content h1,.post-page .post-content h2,.post-page .post-content h3,.post-page .post-content h4,.post-page .post-content h5,.post-page .post-content h6{font-weight:700!important;margin:1.6rem 0 1rem}.post-page .post-actions{text-align:center;margin:1.5rem 0 3rem}.post-page .post-actions .post-meta .info{text-align:left}.post-page .comment-form .card-block{padding:0}.post-page .comment-form .card-block textarea{border:0;padding:1.25rem}.post-page .comment-form .card-footer .btn{font-weight:700;float:right}.post-page .comment-form .card-footer .comment-author-img{height:30px;width:30px}.post-page .card .card-footer{font-size:.8rem;font-weight:300}.post-page .card .comment-author-img{display:inline-block;vertical-align:middle;height:20px;width:20px;border-radius:30px}.post-page .card .comment-author{display:inline-block;vertical-align:middle}.post-page .card .date-posted{display:inline-block;vertical-align:middle;margin-left:5px;color:#bbb}.post-page .card .mod-options{float:right;color:#333;font-size:1rem}.post-page .card .mod-options i{margin-left:5px;opacity:.6;cursor:pointer}.post-page .card .mod-options i:hover{opacity:1}.profile-page .user-info{text-align:center;background:#f3f3f3;padding:2rem 0 1rem}caption,th{text-align:left}fieldset,legend,td,th{padding:0}.profile-page .user-info .user-img{width:100px;height:100px;border-radius:100px;margin-bottom:1rem}.profile-page .user-info h4{font-weight:700}.profile-page .user-info p{margin:0 auto .5rem;color:#aaa;max-width:450px;font-weight:300}.profile-page .user-info .action-btn{float:right;color:#999;border:1px solid #999}.btn-group>.btn-group,.btn-toolbar .btn-group,.btn-toolbar .input-group,.dropdown-menu,.table-reflow thead,.table-reflow tr{float:left}img,legend{border:0}.profile-page .posts-toggle{margin:1.5rem 0 -1px}address,dl,ol,p,ul{margin-bottom:1rem}.editor-page .tag-list i{font-size:.6rem;margin-right:5px;cursor:pointer}/*! + * Bootstrap v4.0.0-alpha.2 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css commit fe56763 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}dd,h1,h2,h3,h4,h5,h6,label{margin-bottom:.5rem}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}address,legend{line-height:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}.custom-select,input[type=search]{-webkit-appearance:none}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,::after,::before{text-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #999}thead{display:table-header-group}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}label,output{display:inline-block}html{box-sizing:border-box;font-size:16px;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}*,::after,::before{box-sizing:inherit}@-ms-viewport{width:device-width}@-o-viewport{width:device-width}@viewport{width:device-width}body{margin:0;font-family:"Source Sans Pro",sans-serif;font-size:1rem;line-height:1.5;color:#373a3c;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}dl,h1,h2,h3,h4,h5,h6,ol,p,ul{margin-top:0}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #818a91}address{font-style:normal}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-left:0}blockquote,figure{margin:0 0 1rem}a{color:#5CB85C;text-decoration:none}a:focus,a:hover{color:#3d8b3d;text-decoration:underline}a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}[role=button]{cursor:pointer}[role=button],a,area,button,input,label,select,summary,textarea{touch-action:manipulation}table{background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#818a91;caption-side:bottom}button:focus{outline:dotted 1px;outline:-webkit-focus-ring-color auto 5px}button,input,select,textarea{margin:0;line-height:inherit;border-radius:0}fieldset{min-width:0;margin:0;border:0}legend{display:block;width:100%;margin-bottom:.5rem;font-size:1.5rem}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:"Merriweather Sans",sans-serif;font-weight:500;line-height:1.1;color:inherit}.blockquote,hr{margin-bottom:1rem}.display-1,.display-2,.display-3,.display-4,.lead{font-weight:300}.h1,h1{font-size:2.5rem}h2{font-size:2rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}.h2{font-size:2rem}.h3{font-size:1.75rem}.h4{font-size:1.5rem}.h5{font-size:1.25rem}.h6{font-size:1rem}.lead{font-size:1.25rem}.display-1{font-size:6rem}.display-2{font-size:5.5rem}.display-3{font-size:4.5rem}.display-4{font-size:3.5rem}hr{box-sizing:content-box;height:0;margin-top:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.alert-link,.close,.label,kbd kbd{font-weight:700}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:5px}.container,.container-fluid{margin-left:auto;margin-right:auto}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote-footer{display:block;font-size:80%;line-height:1.5;color:#818a91}.blockquote-footer::before{content:"\2014 \00A0"}.blockquote-reverse .blockquote-footer::before,.btn-group-vertical>.btn-group::after,.btn-toolbar::after,.custom-controls-stacked .custom-control::after,.dropdown-toggle::after{content:""}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse .blockquote-footer::after{content:"\00A0 \2014"}.carousel-inner>.carousel-item>a>img,.carousel-inner>.carousel-item>img,.img-fluid{display:block;max-width:100%;height:auto}.figure,.img-thumbnail{display:inline-block}.img-rounded{border-radius:.3rem}.img-thumbnail{padding:.25rem;line-height:1.5;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;transition:all .2s ease-in-out;max-width:100%;height:auto}code,kbd{padding:.2rem .4rem;font-size:90%}.img-circle{border-radius:50%}.figure-img{margin-bottom:.5rem;line-height:1}.table,pre{margin-bottom:1rem}.figure-caption{font-size:90%;color:#818a91}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}kbd{color:#fff;background-color:#333;border-radius:.2rem}kbd kbd{padding:0;font-size:100%}.btn,.btn-link,.dropdown-item{font-weight:400}pre{display:block;margin-top:0;font-size:90%;line-height:1.5;color:#373a3c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.container,.container-fluid{padding-left:.9375rem;padding-right:.9375rem}.pre-scrollable{max-height:340px;overflow-y:scroll}@media (min-width:544px){.container{max-width:576px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:940px}}@media (min-width:1200px){.container{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-left:-.9375rem;margin-right:-.9375rem}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:.9375rem;padding-right:.9375rem}.col-xs-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xs-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xs-3{flex:0 0 25%;max-width:25%}.col-xs-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xs-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xs-6{flex:0 0 50%;max-width:50%}.col-xs-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xs-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xs-9{flex:0 0 75%;max-width:75%}.col-xs-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xs-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xs-12{flex:0 0 100%;max-width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.33333%}.col-xs-pull-2{right:16.66667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333%}.col-xs-pull-5{right:41.66667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333%}.col-xs-pull-8{right:66.66667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333%}.col-xs-pull-11{right:91.66667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.33333%}.col-xs-push-2{left:16.66667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333%}.col-xs-push-5{left:41.66667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333%}.col-xs-push-8{left:66.66667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333%}.col-xs-push-11{left:91.66667%}.col-xs-push-12{left:100%}.col-xs-offset-1{margin-left:8.33333%}.col-xs-offset-2{margin-left:16.66667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333%}.col-xs-offset-5{margin-left:41.66667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333%}.col-xs-offset-8{margin-left:66.66667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333%}.col-xs-offset-11{margin-left:91.66667%}.col-xs-first{order:-1}.col-xs-last{order:1}@media (min-width:544px){.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.33333%}.col-sm-pull-2{right:16.66667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333%}.col-sm-pull-5{right:41.66667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333%}.col-sm-pull-8{right:66.66667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333%}.col-sm-pull-11{right:91.66667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.33333%}.col-sm-push-2{left:16.66667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333%}.col-sm-push-5{left:41.66667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333%}.col-sm-push-8{left:66.66667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333%}.col-sm-push-11{left:91.66667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-first{order:-1}.col-sm-last{order:1}}@media (min-width:768px){.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.33333%}.col-md-pull-2{right:16.66667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333%}.col-md-pull-5{right:41.66667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333%}.col-md-pull-8{right:66.66667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333%}.col-md-pull-11{right:91.66667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.33333%}.col-md-push-2{left:16.66667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333%}.col-md-push-5{left:41.66667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333%}.col-md-push-8{left:66.66667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333%}.col-md-push-11{left:91.66667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-first{order:-1}.col-md-last{order:1}}@media (min-width:992px){.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.33333%}.col-lg-pull-2{right:16.66667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333%}.col-lg-pull-5{right:41.66667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333%}.col-lg-pull-8{right:66.66667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333%}.col-lg-pull-11{right:91.66667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.33333%}.col-lg-push-2{left:16.66667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333%}.col-lg-push-5{left:41.66667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333%}.col-lg-push-8{left:66.66667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333%}.col-lg-push-11{left:91.66667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-first{order:-1}.col-lg-last{order:1}}@media (min-width:1200px){.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.col-xl-pull-0{right:auto}.col-xl-pull-1{right:8.33333%}.col-xl-pull-2{right:16.66667%}.col-xl-pull-3{right:25%}.col-xl-pull-4{right:33.33333%}.col-xl-pull-5{right:41.66667%}.col-xl-pull-6{right:50%}.col-xl-pull-7{right:58.33333%}.col-xl-pull-8{right:66.66667%}.col-xl-pull-9{right:75%}.col-xl-pull-10{right:83.33333%}.col-xl-pull-11{right:91.66667%}.col-xl-pull-12{right:100%}.col-xl-push-0{left:auto}.col-xl-push-1{left:8.33333%}.col-xl-push-2{left:16.66667%}.col-xl-push-3{left:25%}.col-xl-push-4{left:33.33333%}.col-xl-push-5{left:41.66667%}.col-xl-push-6{left:50%}.col-xl-push-7{left:58.33333%}.col-xl-push-8{left:66.66667%}.col-xl-push-9{left:75%}.col-xl-push-10{left:83.33333%}.col-xl-push-11{left:91.66667%}.col-xl-push-12{left:100%}.col-xl-offset-0{margin-left:0}.col-xl-offset-1{margin-left:8.33333%}.col-xl-offset-2{margin-left:16.66667%}.col-xl-offset-3{margin-left:25%}.col-xl-offset-4{margin-left:33.33333%}.col-xl-offset-5{margin-left:41.66667%}.col-xl-offset-6{margin-left:50%}.col-xl-offset-7{margin-left:58.33333%}.col-xl-offset-8{margin-left:66.66667%}.col-xl-offset-9{margin-left:75%}.col-xl-offset-10{margin-left:83.33333%}.col-xl-offset-11{margin-left:91.66667%}.col-xl-first{order:-1}.col-xl-last{order:1}}.row-xs-top{align-items:flex-start}.row-xs-center{align-items:center}.row-xs-bottom{align-items:flex-end}.col-xs-top{align-self:flex-start}.col-xs-center,.media-middle{align-self:center}.col-xs-bottom{align-self:flex-end}@media (min-width:544px){.row-sm-top{align-items:flex-start}.row-sm-center{align-items:center}.row-sm-bottom{align-items:flex-end}.col-sm-top{align-self:flex-start}.col-sm-center{align-self:center}.col-sm-bottom{align-self:flex-end}}@media (min-width:768px){.row-md-top{align-items:flex-start}.row-md-center{align-items:center}.row-md-bottom{align-items:flex-end}.col-md-top{align-self:flex-start}.col-md-center{align-self:center}.col-md-bottom{align-self:flex-end}}@media (min-width:992px){.row-lg-top{align-items:flex-start}.row-lg-center{align-items:center}.row-lg-bottom{align-items:flex-end}.col-lg-top{align-self:flex-start}.col-lg-center{align-self:center}.col-lg-bottom{align-self:flex-end}}@media (min-width:1200px){.row-xl-top{align-items:flex-start}.row-xl-center{align-items:center}.row-xl-bottom{align-items:flex-end}.col-xl-top{align-self:flex-start}.col-xl-center{align-self:center}.col-xl-bottom{align-self:flex-end}}.table{width:100%;max-width:100%}.table td,.table th{padding:.75rem;line-height:1.5;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:#f9f9f9}.table-active,.table-active>td,.table-active>th,.table-hover tbody tr:hover{background-color:#f5f5f5}.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:#e8e8e8}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.table-responsive{display:block;width:100%;min-height:.01%;overflow-x:auto}.carousel-inner,.collapsing,.embed-responsive,.modal,.modal-open,.navbar-divider,.sr-only{overflow:hidden}.thead-inverse th{color:#fff;background-color:#373a3c}.thead-default th{color:#55595c;background-color:#eceeef}.table-inverse{color:#eceeef;background-color:#373a3c}.table-inverse.table-bordered{border:0}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#55595c}.table-reflow tbody{display:block;white-space:nowrap}.table-reflow td,.table-reflow th{border-top:1px solid #eceeef;border-left:1px solid #eceeef}.table-reflow td:last-child,.table-reflow th:last-child{border-right:1px solid #eceeef}.table-reflow tbody:last-child tr:last-child td,.table-reflow tbody:last-child tr:last-child th,.table-reflow tfoot:last-child tr:last-child td,.table-reflow tfoot:last-child tr:last-child th,.table-reflow thead:last-child tr:last-child td,.table-reflow thead:last-child tr:last-child th{border-bottom:1px solid #eceeef}.table-reflow tr td,.table-reflow tr th{display:block!important;border:1px solid #eceeef}.form-control,.form-control-file,.form-control-range{display:block}.form-control{width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#55595c;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:.25rem}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{border-color:#66afe9;outline:0}.form-control::placeholder{color:#999;opacity:1}.has-success .checkbox,.has-success .checkbox-inline,.has-success .form-control-feedback,.has-success .form-control-label,.has-success .radio,.has-success .radio-inline,.has-success .text-help,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#5cb85c}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}.form-control-label{padding:.375rem .75rem;margin-bottom:0}_::-webkit-full-page-media.form-control,input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:2.25rem}.input-group-sm _::-webkit-full-page-media.form-control,.input-group-sm input[type=date].form-control,.input-group-sm input[type=time].form-control,.input-group-sm input[type=datetime-local].form-control,.input-group-sm input[type=month].form-control,_::-webkit-full-page-media.input-sm,input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:1.8625rem}.input-group-lg _::-webkit-full-page-media.form-control,.input-group-lg input[type=date].form-control,.input-group-lg input[type=time].form-control,.input-group-lg input[type=datetime-local].form-control,.input-group-lg input[type=month].form-control,_::-webkit-full-page-media.input-lg,input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:3.16667rem}.form-control-static{min-height:2.25rem;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.275rem .75rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.25rem;font-size:1.25rem;line-height:1.33333;border-radius:.3rem}.form-group{margin-bottom:1rem}.checkbox,.radio{display:block;margin-bottom:.75rem}.checkbox label,.checkbox-inline,.radio label,.radio-inline{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.checkbox label input:only-child,.radio label input:only-child{position:static}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.collapsing,.dropdown,.dropup{position:relative}.checkbox+.checkbox,.radio+.radio{margin-top:-.25rem}.checkbox-inline,.radio-inline{position:relative;display:inline-block;vertical-align:middle}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:.75rem}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,input[type=checkbox].disabled,input[type=checkbox]:disabled,input[type=radio].disabled,input[type=radio]:disabled{cursor:not-allowed}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;background-size:1.4625rem 1.4625rem}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .form-control-feedback,.has-warning .form-control-label,.has-warning .radio,.has-warning .radio-inline,.has-warning .text-help,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#f0ad4e}.has-success .form-control-success{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg==)}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-danger .checkbox,.has-danger .checkbox-inline,.has-danger .form-control-feedback,.has-danger .form-control-label,.has-danger .radio,.has-danger .radio-inline,.has-danger .text-help,.has-danger.checkbox label,.has-danger.checkbox-inline label,.has-danger.radio label,.has-danger.radio-inline label{color:#B85C5C}.has-warning .form-control-warning{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+)}.has-danger .form-control{border-color:#B85C5C}.has-danger .input-group-addon{color:#B85C5C;border-color:#B85C5C;background-color:#f6eaea}.has-danger .form-control-danger{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4=)}@media (min-width:544px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .form-control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.btn-block,input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.btn{display:inline-block;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;user-select:none;border:1px solid transparent;padding:.375rem 1rem;font-size:1rem;line-height:1.5;border-radius:.25rem}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#5CB85C;border-color:#5CB85C}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-primary.active,.btn-primary:active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#419641;background-image:none}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.btn-primary.dropdown-toggle.focus,.open>.btn-primary.dropdown-toggle:focus,.open>.btn-primary.dropdown-toggle:hover{color:#fff;background-color:#398439;border-color:#2d672d}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary:disabled.focus,.btn-primary:disabled:focus,.btn-primary:disabled:hover{background-color:#5CB85C;border-color:#5CB85C}.btn-secondary{color:#373a3c;background-color:#fff;border-color:#ccc}.btn-secondary.focus,.btn-secondary:focus,.btn-secondary:hover{color:#373a3c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.active,.btn-secondary:active,.open>.btn-secondary.dropdown-toggle{color:#373a3c;background-color:#e6e6e6;border-color:#adadad;background-image:none}.btn-secondary.active.focus,.btn-secondary.active:focus,.btn-secondary.active:hover,.btn-secondary:active.focus,.btn-secondary:active:focus,.btn-secondary:active:hover,.open>.btn-secondary.dropdown-toggle.focus,.open>.btn-secondary.dropdown-toggle:focus,.open>.btn-secondary.dropdown-toggle:hover{color:#373a3c;background-color:#d4d4d4;border-color:#8c8c8c}.btn-secondary.disabled.focus,.btn-secondary.disabled:focus,.btn-secondary.disabled:hover,.btn-secondary:disabled.focus,.btn-secondary:disabled:focus,.btn-secondary:disabled:hover{background-color:#fff;border-color:#ccc}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info.focus,.btn-info:focus,.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.active,.btn-info:active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#2aabd2;background-image:none}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.btn-info.dropdown-toggle.focus,.open>.btn-info.dropdown-toggle:focus,.open>.btn-info.dropdown-toggle:hover{color:#fff;background-color:#269abc;border-color:#1f7e9a}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info:disabled.focus,.btn-info:disabled:focus,.btn-info:disabled:hover{background-color:#5bc0de;border-color:#5bc0de}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success.focus,.btn-success:focus,.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.active,.btn-success:active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#419641;background-image:none}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.btn-success.dropdown-toggle.focus,.open>.btn-success.dropdown-toggle:focus,.open>.btn-success.dropdown-toggle:hover{color:#fff;background-color:#398439;border-color:#2d672d}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success:disabled.focus,.btn-success:disabled:focus,.btn-success:disabled:hover{background-color:#5cb85c;border-color:#5cb85c}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.focus,.btn-warning:focus,.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.active,.btn-warning:active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#eb9316;background-image:none}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.btn-warning.dropdown-toggle.focus,.open>.btn-warning.dropdown-toggle:focus,.open>.btn-warning.dropdown-toggle:hover{color:#fff;background-color:#d58512;border-color:#b06d0f}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning:disabled.focus,.btn-warning:disabled:focus,.btn-warning:disabled:hover{background-color:#f0ad4e;border-color:#f0ad4e}.btn-danger{color:#fff;background-color:#B85C5C;border-color:#B85C5C}.btn-danger.focus,.btn-danger:focus,.btn-danger:hover{color:#fff;background-color:#9d4444;border-color:#964141}.btn-danger.active,.btn-danger:active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#9d4444;border-color:#964141;background-image:none}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.btn-danger.dropdown-toggle.focus,.open>.btn-danger.dropdown-toggle:focus,.open>.btn-danger.dropdown-toggle:hover{color:#fff;background-color:#843939;border-color:#672d2d}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger:disabled.focus,.btn-danger:disabled:focus,.btn-danger:disabled:hover{background-color:#B85C5C;border-color:#B85C5C}.btn-outline-primary{color:#5CB85C;background-image:none;background-color:transparent;border-color:#5CB85C}.btn-outline-primary.active,.btn-outline-primary.focus,.btn-outline-primary:active,.btn-outline-primary:focus,.btn-outline-primary:hover,.open>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#5CB85C;border-color:#5CB85C}.btn-outline-primary.disabled.focus,.btn-outline-primary.disabled:focus,.btn-outline-primary.disabled:hover,.btn-outline-primary:disabled.focus,.btn-outline-primary:disabled:focus,.btn-outline-primary:disabled:hover{border-color:#a3d7a3}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary.active,.btn-outline-secondary.focus,.btn-outline-secondary:active,.btn-outline-secondary:focus,.btn-outline-secondary:hover,.open>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.disabled.focus,.btn-outline-secondary.disabled:focus,.btn-outline-secondary.disabled:hover,.btn-outline-secondary:disabled.focus,.btn-outline-secondary:disabled:focus,.btn-outline-secondary:disabled:hover{border-color:#fff}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info.active,.btn-outline-info.focus,.btn-outline-info:active,.btn-outline-info:focus,.btn-outline-info:hover,.open>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.disabled.focus,.btn-outline-info.disabled:focus,.btn-outline-info.disabled:hover,.btn-outline-info:disabled.focus,.btn-outline-info:disabled:focus,.btn-outline-info:disabled:hover{border-color:#b0e1ef}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success.active,.btn-outline-success.focus,.btn-outline-success:active,.btn-outline-success:focus,.btn-outline-success:hover,.open>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.disabled.focus,.btn-outline-success.disabled:focus,.btn-outline-success.disabled:hover,.btn-outline-success:disabled.focus,.btn-outline-success:disabled:focus,.btn-outline-success:disabled:hover{border-color:#a3d7a3}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning.active,.btn-outline-warning.focus,.btn-outline-warning:active,.btn-outline-warning:focus,.btn-outline-warning:hover,.open>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.disabled.focus,.btn-outline-warning.disabled:focus,.btn-outline-warning.disabled:hover,.btn-outline-warning:disabled.focus,.btn-outline-warning:disabled:focus,.btn-outline-warning:disabled:hover{border-color:#f8d9ac}.btn-outline-danger{color:#B85C5C;background-image:none;background-color:transparent;border-color:#B85C5C}.btn-outline-danger.active,.btn-outline-danger.focus,.btn-outline-danger:active,.btn-outline-danger:focus,.btn-outline-danger:hover,.open>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#B85C5C;border-color:#B85C5C}.btn-outline-danger.disabled.focus,.btn-outline-danger.disabled:focus,.btn-outline-danger.disabled:hover,.btn-outline-danger:disabled.focus,.btn-outline-danger:disabled:focus,.btn-outline-danger:disabled:hover{border-color:#d7a3a3}.btn-link{color:#5CB85C;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#3d8b3d;text-decoration:underline;background-color:transparent}.btn-link:disabled:focus,.btn-link:disabled:hover{color:#818a91;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.25rem;font-size:1.25rem;line-height:1.33333;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .75rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block}.btn-block+.btn-block{margin-top:5px}.fade{opacity:0;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{height:0;transition-timing-function:ease;transition-duration:.35s;transition-property:height}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-right:.25rem;margin-left:.25rem;vertical-align:middle;border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:1rem;color:#373a3c;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-header,.dropdown-item{display:block;padding:3px 20px;line-height:1.5;white-space:nowrap}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#e5e5e5}.dropdown-item{width:100%;clear:both;color:#373a3c;text-align:inherit;background:0 0;border:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle),.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.dropdown-item:focus,.dropdown-item:hover{color:#2b2d2f;text-decoration:none;background-color:#f5f5f5}.dropdown-item.active,.dropdown-item.active:focus,.dropdown-item.active:hover{color:#fff;text-decoration:none;background-color:#5CB85C;outline:0}.dropdown-item.disabled,.dropdown-item.disabled:focus,.dropdown-item.disabled:hover{color:#818a91}.dropdown-item.disabled:focus,.dropdown-item.disabled:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:"progid:DXImageTransform.Microsoft.gradient(enabled = false)"}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{font-size:.875rem;color:#818a91}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:.3em solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar::after{display:table;clear:both}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group-lg.btn-group>.btn+.dropdown-toggle,.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group-lg>.btn .caret,.btn-lg .caret{border-width:.3em .3em 0}.dropup .btn-group-lg>.btn .caret,.dropup .btn-lg .caret{border-width:0 .3em .3em}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group::after{display:table;clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.custom-control,.input-group,.input-group-btn,.input-group-btn>.btn{position:relative}.input-group{display:flex}.input-group .form-control{position:relative;z-index:2;flex:1;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover,.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#55595c;text-align:center;background-color:#eceeef;border:1px solid #ccc;border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.275rem .75rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.25rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.custom-control+.custom-control,.nav-inline .nav-item+.nav-item,.nav-inline .nav-link+.nav-link{margin-left:1rem}.input-group-btn:last-child>.btn-group:active,.input-group-btn:last-child>.btn-group:focus,.input-group-btn:last-child>.btn-group:hover,.input-group-btn:last-child>.btn:active,.input-group-btn:last-child>.btn:focus,.input-group-btn:last-child>.btn:hover{z-index:3}.custom-control{display:inline;padding-left:1.5rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0074d9}.custom-control-input:focus~.custom-control-indicator{box-shadow:0 0 0 .075rem #fff,0 0 0 .2rem #0074d9}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#84c6ff}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eee}.custom-control-input:disabled~.custom-control-description{color:#767676;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.0625rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTYuNCwxTDUuNywxLjdMMi45LDQuNUwyLjEsMy43TDEuNCwzTDAsNC40bDAuNywwLjdsMS41LDEuNWwwLjcsMC43bDAuNy0wLjdsMy41LTMuNWwwLjctMC43TDYuNCwxTDYuNCwxeiINCgkvPg0KPC9zdmc+DQo=)}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0074d9;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iOHB4IiBoZWlnaHQ9IjhweCIgdmlld0JveD0iMCAwIDggOCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOCA4IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0wLDN2Mmg4VjNIMHoiLz4NCjwvc3ZnPg0K)}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQsMUMyLjMsMSwxLDIuMywxLDRzMS4zLDMsMywzczMtMS4zLDMtM1M1LjcsMSw0LDF6Ii8+DQo8L3N2Zz4NCg==)}.custom-controls-stacked .custom-control{display:inline}.custom-controls-stacked .custom-control::after{display:block;margin-bottom:.25rem}.custom-file,.custom-select{display:inline-block;max-width:100%}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{padding:.375rem 1.75rem .375rem .75rem;padding-right:.75rem\9;color:#55595c;vertical-align:middle;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC) right .75rem center no-repeat #fff;background-image:none\9;background-size:8px 10px;border:1px solid #ccc;border-radius:.25rem;-moz-appearance:none}.custom-select:focus{border-color:#51a7e8;outline:0}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;height:2.5rem;cursor:pointer}.custom-file-control,.custom-file-control::before{position:absolute;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#555}.custom-file-input{min-width:14rem;max-width:100%;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{top:0;right:0;left:0;z-index:5;user-select:none;background-color:#fff;border:1px solid #ddd;border-radius:.25rem}.custom-file-control::after{content:"Choose file..."}.custom-file-control::before{top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;content:"Browse";background-color:#eee;border:1px solid #ddd;border-radius:0 .25rem .25rem 0}.nav-inline .nav-item,.nav-link{display:inline-block}.nav-pills::after,.nav-tabs::after,.navbar::after{content:"";clear:both}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#818a91}.nav-link.disabled,.nav-link.disabled:focus,.nav-link.disabled:hover{color:#818a91;cursor:not-allowed;background-color:transparent}.nav-pills .nav-item+.nav-item,.nav-tabs .nav-item+.nav-item{margin-left:.2rem}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs::after{display:table}.nav-tabs .nav-item{float:left;margin-bottom:-1px}.card,.card-title{margin-bottom:.75rem}.nav-tabs .nav-link{display:block;padding:.5em 1em;border:1px solid transparent;border-radius:.25rem .25rem 0 0}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link.disabled:focus,.nav-tabs .nav-link.disabled:hover{color:#818a91;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover{color:#55595c;background-color:#fff;border-color:#ddd #ddd transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills::after{display:table}.nav-pills .nav-item{float:left}.nav-pills .nav-link{display:block;padding:.5em 1em;border-radius:.25rem}.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover{color:#fff;cursor:default;background-color:#5CB85C}.nav-stacked .nav-item{display:block;float:none}.breadcrumb-item,.navbar-brand,.navbar-nav .nav-item,.page-link{float:left}.nav-stacked .nav-item+.nav-item{margin-top:.2rem;margin-left:0}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;padding:.5rem 0!important}.navbar::after{display:table}.navbar-full{z-index:1000}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0}.navbar-fixed-bottom{bottom:0}.navbar-sticky-top{position:sticky;top:0;z-index:1030;width:100%}@media (min-width:544px){.navbar{border-radius:.25rem}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-full,.navbar-sticky-top{border-radius:0}}.navbar-brand{padding-bottom:.25rem}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}.navbar-divider{float:left;width:1px;padding-top:.425rem;padding-bottom:.425rem;margin-right:0!important;margin-left:0!important}.navbar-divider::before{content:"\00a0"}.navbar-toggler{padding:.5rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}@media (min-width:544px){.navbar-toggleable-xs{display:block!important}}@media (min-width:768px){.navbar-toggleable-sm{display:block!important}}@media (min-width:992px){.navbar-toggleable-md{display:block!important}}.navbar-nav .nav-link{display:block;padding-top:.425rem;padding-bottom:.425rem}.navbar-nav .nav-item+.nav-item,.navbar-nav .nav-link+.nav-link{margin-left:1rem}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.8)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.6)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .active>.nav-link:focus,.navbar-light .navbar-nav .active>.nav-link:hover,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.active:focus,.navbar-light .navbar-nav .nav-link.active:hover,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .nav-link.open:focus,.navbar-light .navbar-nav .nav-link.open:hover,.navbar-light .navbar-nav .open>.nav-link,.navbar-light .navbar-nav .open>.nav-link:focus,.navbar-light .navbar-nav .open>.nav-link:hover{color:rgba(0,0,0,.8)}.navbar-light .navbar-divider{background-color:rgba(0,0,0,.075)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .active>.nav-link:focus,.navbar-dark .navbar-nav .active>.nav-link:hover,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.active:focus,.navbar-dark .navbar-nav .nav-link.active:hover,.navbar-dark .navbar-nav .nav-link.open,.navbar-dark .navbar-nav .nav-link.open:focus,.navbar-dark .navbar-nav .nav-link.open:hover,.navbar-dark .navbar-nav .open>.nav-link,.navbar-dark .navbar-nav .open>.nav-link:focus,.navbar-dark .navbar-nav .open>.nav-link:hover{color:#fff}.navbar-dark .navbar-divider{background-color:rgba(255,255,255,.075)}.card{position:relative;display:block;background-color:#fff;border:1px solid #e5e5e5;border-radius:.25rem}.card-block::after,.card-footer::after,.card-header::after{display:table;content:"";clear:both}.card-block{padding:1.25rem}.card-footer,.card-header{padding:.75rem 1.25rem;background-color:#f5f5f5}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-subtitle{margin-top:-.375rem}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-radius:.25rem .25rem 0 0}.card>.list-group:last-child .list-group-item:last-child{border-radius:0 0 .25rem .25rem}.card-header{border-bottom:1px solid #e5e5e5}.card-header:first-child{border-radius:.25rem .25rem 0 0}.card-footer{border-top:1px solid #e5e5e5}.card-footer:last-child{border-radius:0 0 .25rem .25rem}.card-primary{background-color:#5CB85C;border-color:#5CB85C}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-danger{background-color:#B85C5C;border-color:#B85C5C}.card-outline-danger,.card-outline-info,.card-outline-primary,.card-outline-secondary,.card-outline-success,.card-outline-warning{background-color:transparent}.card-outline-primary{border-color:#5CB85C}.card-outline-secondary{border-color:#ccc}.card-outline-info{border-color:#5bc0de}.card-outline-success{border-color:#5cb85c}.card-outline-warning{border-color:#f0ad4e}.card-outline-danger{border-color:#B85C5C}.card-inverse .card-footer,.card-inverse .card-header{border-bottom:1px solid rgba(255,255,255,.2)}.card-inverse .card-blockquote>footer,.card-inverse .card-link,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:.25rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-radius:.25rem .25rem 0 0}.card-img-bottom{border-radius:0 0 .25rem .25rem}@media (min-width:544px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-.625rem;margin-left:-.625rem}.card-deck .card{flex:1 0 0;margin-right:.625rem;margin-left:.625rem}.card-group{display:flex;flex-flow:row wrap}.card-group .card{flex:1 0 0}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child),.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}.card-columns{column-count:3;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%}}.breadcrumb,.pagination{margin-bottom:1rem;border-radius:.25rem}.breadcrumb{padding:.75rem 1rem;list-style:none;background-color:#eceeef}.breadcrumb::after{content:"";display:table;clear:both}.label,.pagination{display:inline-block}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#818a91;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#818a91}.pagination{padding-left:0;margin-top:1rem}.page-item{display:inline}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link,.page-item.active .page-link:focus,.page-item.active .page-link:hover{z-index:2;color:#fff;cursor:default;background-color:#5CB85C;border-color:#5CB85C}.page-item.disabled .page-link,.page-item.disabled .page-link:focus,.page-item.disabled .page-link:hover{color:#818a91;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;padding:.5rem .75rem;margin-left:-1px;line-height:1.5;color:#5CB85C;text-decoration:none;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#3d8b3d;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.33333}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.275rem .75rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.label{padding:.25em .4em;font-size:75%;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.label-default{background-color:#818a91}.label-default[href]:focus,.label-default[href]:hover{background-color:#687077}.label-primary{background-color:#5CB85C}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#449d44}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#B85C5C}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#9d4444}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:544px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:15px;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert>p,.alert>ul{margin-bottom:0}.media,.progress{margin-bottom:1rem}.alert>p+p{margin-top:5px}.alert-heading{color:inherit}.alert-dismissible{padding-right:35px}.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:block;width:100%;height:1rem}.progress[value]{background-color:#eee;border:0;appearance:none;border-radius:.25rem}.progress[value]::-ms-fill{background-color:#0074d9;border:0}.progress[value]::-moz-progress-bar{background-color:#0074d9;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.progress[value]::-webkit-progress-value{background-color:#0074d9;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.progress[value="100"]::-moz-progress-bar{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.progress[value="100"]::-webkit-progress-value{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.progress[value]::-webkit-progress-bar{background-color:#eee;border-radius:.25rem}.progress[value],base::-moz-progress-bar{background-color:#eee;border-radius:.25rem}.progress-striped[value]::-webkit-progress-value{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-striped[value]::-moz-progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-striped[value]::-ms-fill{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-animated[value]::-webkit-progress-value{animation:progress-bar-stripes 2s linear infinite}.progress-animated[value]::-moz-progress-bar{animation:progress-bar-stripes 2s linear infinite}@media screen and (min-width:0\0){.progress{background-color:#eee;border-radius:.25rem}.progress-bar{display:inline-block;height:1rem;text-indent:-999rem;background-color:#0074d9;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.progress[width="100%"]{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-animated .progress-bar-striped{animation:progress-bar-stripes 2s linear infinite}.progress-success .progress-bar{background-color:#5cb85c}}.progress-success[value]::-webkit-progress-value{background-color:#5cb85c}.progress-success[value]::-moz-progress-bar{background-color:#5cb85c}.progress-success[value]::-ms-fill{background-color:#5cb85c}.progress-info[value]::-webkit-progress-value{background-color:#5bc0de}.progress-info[value]::-moz-progress-bar{background-color:#5bc0de}.progress-info[value]::-ms-fill{background-color:#5bc0de}@media screen and (min-width:0\0){.progress-info .progress-bar{background-color:#5bc0de}.progress-warning .progress-bar{background-color:#f0ad4e}}.progress-warning[value]::-webkit-progress-value{background-color:#f0ad4e}.progress-warning[value]::-moz-progress-bar{background-color:#f0ad4e}.progress-warning[value]::-ms-fill{background-color:#f0ad4e}.progress-danger[value]::-webkit-progress-value{background-color:#B85C5C}.progress-danger[value]::-moz-progress-bar{background-color:#B85C5C}.progress-danger[value]::-ms-fill{background-color:#B85C5C}@media screen and (min-width:0\0){.progress-danger .progress-bar{background-color:#B85C5C}}.media{display:flex}.media-body{flex:1}.media-bottom{align-self:flex-end}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right{padding-left:10px}.media-left{padding-right:10px}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:0}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-flush .list-group-item{border-width:1px 0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}a.list-group-item,button.list-group-item{width:100%;color:#555;text-align:inherit}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#818a91;cursor:not-allowed;background-color:#eceeef}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#818a91}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#5CB85C;border-color:#5CB85C}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#eaf6ea}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9{padding-bottom:42.85714%}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.embed-responsive-1by1{padding-bottom:100%}.close{float:right;font-size:1.5rem;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2}.popover,.tooltip{font-family:"Source Sans Pro",sans-serif;font-style:normal;letter-spacing:normal;line-break:auto;line-height:1.5;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;text-decoration:none;font-weight:400}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;outline:0;-webkit-overflow-scrolling:touch}.modal-footer::after,.modal-header::after{display:table;content:"";clear:both}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-25%)}.modal.in .modal-dialog{transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.in{opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.5}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.popover,.tooltip{position:absolute;display:block}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:544px){.modal-dialog{width:600px;margin:30px auto}.modal-sm{width:300px}}@media (min-width:768px){.modal-lg{width:900px}}.tooltip{z-index:1070;text-align:left;text-align:start;opacity:0}.tooltip.in{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-arrow,.tooltip.tooltip-top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-arrow,.tooltip.tooltip-right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-arrow,.tooltip.tooltip-bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-arrow,.tooltip.tooltip-left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{top:0;left:0;z-index:1060;max-width:276px;padding:1px;text-align:left;text-align:start;background-color:#fff;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.carousel-caption,.carousel-control{color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom .popover-arrow,.popover.popover-top .popover-arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.bs-tether-element-attached-bottom .popover-arrow::after,.popover.popover-top .popover-arrow::after{bottom:1px;margin-left:-10px;content:"";border-top-color:#fff;border-bottom-width:0}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left .popover-arrow,.popover.popover-right .popover-arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.bs-tether-element-attached-left .popover-arrow::after,.popover.popover-right .popover-arrow::after{bottom:-10px;left:1px;content:"";border-right-color:#fff;border-left-width:0}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top .popover-arrow,.popover.popover-bottom .popover-arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top .popover-arrow::after,.popover.popover-bottom .popover-arrow::after{top:1px;margin-left:-10px;content:"";border-top-width:0;border-bottom-color:#fff}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right .popover-arrow,.popover.popover-left .popover-arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right .popover-arrow::after,.popover.popover-left .popover-arrow::after{right:1px;bottom:-10px;content:"";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:.2375rem .2375rem 0 0}.popover-content{padding:9px 14px}.popover-arrow,.popover-arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover-arrow{border-width:11px}.popover-arrow::after{content:"";border-width:10px}.carousel-inner{width:100%}.carousel-inner>.carousel-item{position:relative;display:none;transition:.6s ease-in-out left}.carousel-inner>.carousel-item>a>img,.carousel-inner>.carousel-item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.carousel-item{transition:transform .6s ease-in-out;backface-visibility:hidden;perspective:1000px}.carousel-inner>.carousel-item.active.right,.carousel-inner>.carousel-item.next{left:0;transform:translate3d(100%,0,0)}.carousel-inner>.carousel-item.active.left,.carousel-inner>.carousel-item.prev{left:0;transform:translate3d(-100%,0,0)}.carousel-inner>.carousel-item.active,.carousel-inner>.carousel-item.next.left,.carousel-inner>.carousel-item.prev.right{left:0;transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;opacity:.5}.carousel-control.left{background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-prev::before{content:"\2039"}.carousel-control .icon-next::before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:transparent;border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media (min-width:544px){.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .icon-prev{margin-left:-15px}.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.bg-inverse{color:#eceeef;background-color:#373a3c}.bg-danger,.bg-info,.bg-primary,.bg-success,.bg-warning{color:#fff!important}.bg-faded{background-color:#f7f7f9}.bg-primary{background-color:#5CB85C!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#449d44!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#B85C5C!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#9d4444!important}.center-block{display:block;margin-left:auto;margin-right:auto}.clearfix::after{content:"";display:table;clear:both}.hidden-xl-down,.hidden-xs-up,.visible-print-block{display:none!important}.pull-xs-left{float:left!important}.pull-xs-right{float:right!important}.pull-xs-none{float:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;clip:rect(0,0,0,0);border:0}.p-r-0,.p-x-0{padding-right:0!important}.p-l-0,.p-x-0{padding-left:0!important}.p-t-0,.p-y-0{padding-top:0!important}.p-b-0,.p-y-0{padding-bottom:0!important}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.m-t-0,.m-y-0{margin-top:0!important}.m-b-0,.m-y-0{margin-bottom:0!important}.m-x-auto{margin-right:auto!important;margin-left:auto!important}.m-r-0,.m-x-0{margin-right:0!important}.m-l-0,.m-x-0{margin-left:0!important}.m-a-0{margin:0!important}.m-r-1,.m-x-1{margin-right:1rem!important}.m-l-1,.m-x-1{margin-left:1rem!important}.m-t-1,.m-y-1{margin-top:1rem!important}.m-b-1,.m-y-1{margin-bottom:1rem!important}.m-a-1{margin:1rem!important}.m-r-2,.m-x-2{margin-right:1.5rem!important}.m-l-2,.m-x-2{margin-left:1.5rem!important}.m-t-2,.m-y-2{margin-top:1.5rem!important}.m-b-2,.m-y-2{margin-bottom:1.5rem!important}.m-a-2{margin:1.5rem!important}.m-r-3,.m-x-3{margin-right:3rem!important}.m-l-3,.m-x-3{margin-left:3rem!important}.m-t-3,.m-y-3{margin-top:3rem!important}.m-b-3,.m-y-3{margin-bottom:3rem!important}.m-a-3{margin:3rem!important}.p-a-0{padding:0!important}.p-r-1,.p-x-1{padding-right:1rem!important}.p-l-1,.p-x-1{padding-left:1rem!important}.p-t-1,.p-y-1{padding-top:1rem!important}.p-b-1,.p-y-1{padding-bottom:1rem!important}.p-a-1{padding:1rem!important}.p-r-2,.p-x-2{padding-right:1.5rem!important}.p-l-2,.p-x-2{padding-left:1.5rem!important}.p-t-2,.p-y-2{padding-top:1.5rem!important}.p-b-2,.p-y-2{padding-bottom:1.5rem!important}.p-a-2{padding:1.5rem!important}.p-r-3,.p-x-3{padding-right:3rem!important}.p-l-3,.p-x-3{padding-left:3rem!important}.p-t-3,.p-y-3{padding-top:3rem!important}.p-b-3,.p-y-3{padding-bottom:3rem!important}.p-a-3{padding:3rem!important}.pos-f-t{position:fixed;top:0;right:0;left:0;z-index:1030}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-xs-left{text-align:left!important}.text-xs-right{text-align:right!important}.text-xs-center{text-align:center!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-muted{color:#818a91}.text-primary{color:#5CB85C!important}a.text-primary:focus,a.text-primary:hover{color:#449d44}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f}.text-danger{color:#B85C5C!important}a.text-danger:focus,a.text-danger:hover{color:#9d4444}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.invisible{visibility:hidden!important}@media (max-width:543px){.hidden-xs-down{display:none!important}}@media (min-width:544px){.pull-sm-left{float:left!important}.pull-sm-right{float:right!important}.pull-sm-none{float:none!important}.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.pull-md-left{float:left!important}.pull-md-right{float:right!important}.pull-md-none{float:none!important}.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.pull-lg-left{float:left!important}.pull-lg-right{float:right!important}.pull-lg-none{float:none!important}.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.pull-xl-left{float:left!important}.pull-xl-right{float:right!important}.pull-xl-none{float:none!important}.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}.hidden-xl-up{display:none!important}}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}} .nav-link .user-pic {height: 26px; border-radius: 50px; float: left;margin-right: 5px;} #feed-post-preview .post-meta button { float: right; } .tag-pill { padding-right: 0.6em; padding-left: 0.6em; border-radius: 10rem; } .tag-default { color: #fff important; font-size: 0.8rem; padding-top: 0; padding-bottom: 0.1rem; white-space: nowrap; margin-right: 3px; margin-bottom: 0.2rem; display: inline-block; } .tag-default:hover { text-decoration: none; } .tag-default.tag-outline { border: 1px solid #ddd; color: #aaa !important; background: none !important; } ul.tag-list { padding-left: 0px !important; display: inline-block; list-style: none !important; } ul.tag-list li { display: inline-block !important; } .preview-link ul.tag-list { float: right; max-width: 80%; vertical-align: top; } diff --git a/cmd/web/static/css/tagify.css b/cmd/web/static/css/tagify.css new file mode 100644 index 0000000..009e2d9 --- /dev/null +++ b/cmd/web/static/css/tagify.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--tagify-dd-color-primary:rgb(53,149,246);--tagify-dd-bg-color:white;--tagify-dd-item-pad:.3em .5em}.tagify{--tags-disabled-bg:#F1F1F1;--tags-border-color:#DDD;--tags-hover-border-color:#CCC;--tags-focus-border-color:#3595f6;--tag-border-radius:3px;--tag-bg:#E5E5E5;--tag-hover:#D3E2E2;--tag-text-color:black;--tag-text-color--edit:black;--tag-pad:0.3em 0.5em;--tag-inset-shadow-size:1.1em;--tag-invalid-color:#D39494;--tag-invalid-bg:rgba(211, 148, 148, 0.5);--tag-remove-bg:rgba(211, 148, 148, 0.3);--tag-remove-btn-color:black;--tag-remove-btn-bg:none;--tag-remove-btn-bg--hover:#c77777;--input-color:inherit;--tag--min-width:1ch;--tag--max-width:auto;--tag-hide-transition:0.3s;--placeholder-color:rgba(0, 0, 0, 0.4);--placeholder-color-focus:rgba(0, 0, 0, 0.25);--loader-size:.8em;--readonly-striped:1;display:inline-flex;align-items:flex-start;flex-wrap:wrap;border:1px solid var(--tags-border-color);padding:0;line-height:0;cursor:text;outline:0;position:relative;box-sizing:border-box;transition:.1s}@keyframes tags--bump{30%{transform:scale(1.2)}}@keyframes rotateLoader{to{transform:rotate(1turn)}}.tagify:hover:not(.tagify--focus):not(.tagify--invalid){--tags-border-color:var(--tags-hover-border-color)}.tagify[disabled]{background:var(--tags-disabled-bg);filter:saturate(0);opacity:.5;pointer-events:none}.tagify[disabled].tagify--select,.tagify[readonly].tagify--select{pointer-events:none}.tagify[disabled]:not(.tagify--mix):not(.tagify--select),.tagify[readonly]:not(.tagify--mix):not(.tagify--select){cursor:default}.tagify[disabled]:not(.tagify--mix):not(.tagify--select)>.tagify__input,.tagify[readonly]:not(.tagify--mix):not(.tagify--select)>.tagify__input{visibility:hidden;width:0;margin:5px 0}.tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div,.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div{padding:var(--tag-pad)}.tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div::before,.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div::before{animation:readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused}@keyframes readonlyStyles{0%{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}}.tagify[disabled] .tagify__tag__removeBtn,.tagify[readonly] .tagify__tag__removeBtn{display:none}.tagify--loading .tagify__input>br:last-child{display:none}.tagify--loading .tagify__input::before{content:none}.tagify--loading .tagify__input::after{content:"";vertical-align:middle;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);min-width:0;border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear;content:""!important;margin:-2px 0 -2px .5em}.tagify--loading .tagify__input:empty::after{margin-left:0}.tagify+input,.tagify+textarea{position:absolute!important;left:-9999em!important;transform:scale(0)!important}.tagify__tag{display:inline-flex;align-items:center;margin:5px 0 5px 5px;position:relative;z-index:1;outline:0;line-height:normal;cursor:default;transition:.13s ease-out}.tagify__tag>div{vertical-align:top;box-sizing:border-box;max-width:100%;padding:var(--tag-pad);color:var(--tag-text-color);line-height:inherit;border-radius:var(--tag-border-radius);white-space:nowrap;transition:.13s ease-out}.tagify__tag>div>*{white-space:pre-wrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:top;min-width:var(--tag--min-width);max-width:var(--tag--max-width);transition:.8s ease,.1s color}.tagify__tag>div>[contenteditable]{outline:0;-webkit-user-select:text;user-select:text;cursor:text;margin:-2px;padding:2px;max-width:350px}.tagify__tag>div::before{content:"";position:absolute;border-radius:inherit;inset:var(--tag-bg-inset,0);z-index:-1;pointer-events:none;transition:120ms ease;animation:tags--bump .3s ease-out 1;box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset}.tagify__tag:focus div::before,.tagify__tag:hover:not([readonly]) div::before{--tag-bg-inset:-2.5px;--tag-bg:var(--tag-hover)}.tagify__tag--loading{pointer-events:none}.tagify__tag--loading .tagify__tag__removeBtn{display:none}.tagify__tag--loading::after{--loader-size:.4em;content:"";vertical-align:middle;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);min-width:0;border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear;margin:0 .5em 0 -.1em}.tagify__tag--flash div::before{animation:none}.tagify__tag--hide{width:0!important;padding-left:0;padding-right:0;margin-left:0;margin-right:0;opacity:0;transform:scale(0);transition:var(--tag-hide-transition);pointer-events:none}.tagify__tag--hide>div>*{white-space:nowrap}.tagify__tag.tagify--noAnim>div::before{animation:none}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div>span{opacity:.5}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before{--tag-bg:var(--tag-invalid-bg);transition:.2s}.tagify__tag[readonly] .tagify__tag__removeBtn{display:none}.tagify__tag[readonly]>div::before{animation:readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused}@keyframes readonlyStyles{0%{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}}.tagify__tag--editable>div{color:var(--tag-text-color--edit)}.tagify__tag--editable>div::before{box-shadow:0 0 0 2px var(--tag-hover) inset!important}.tagify__tag--editable>.tagify__tag__removeBtn{pointer-events:none}.tagify__tag--editable>.tagify__tag__removeBtn::after{opacity:0;transform:translateX(100%) translateX(5px)}.tagify__tag--editable.tagify--invalid>div::before{box-shadow:0 0 0 2px var(--tag-invalid-color) inset!important}.tagify__tag__removeBtn{order:5;display:inline-flex;align-items:center;justify-content:center;border-radius:50px;cursor:pointer;font:14px/1 Arial;background:var(--tag-remove-btn-bg);color:var(--tag-remove-btn-color);width:14px;height:14px;margin-right:4.6666666667px;margin-left:auto;overflow:hidden;transition:.2s ease-out}.tagify__tag__removeBtn::after{content:"×";transition:.3s,color 0s}.tagify__tag__removeBtn:hover{color:#fff;background:var(--tag-remove-btn-bg--hover)}.tagify__tag__removeBtn:hover+div>span{opacity:.5}.tagify__tag__removeBtn:hover+div::before{box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg,rgba(211,148,148,.3)) inset!important;transition:box-shadow .2s}.tagify:not(.tagify--mix) .tagify__input br{display:none}.tagify:not(.tagify--mix) .tagify__input *{display:inline;white-space:nowrap}.tagify__input{flex-grow:1;display:inline-block;min-width:110px;margin:5px;padding:var(--tag-pad);line-height:normal;position:relative;white-space:pre-wrap;color:var(--input-color);box-sizing:inherit}.tagify__input:empty::before{position:static}.tagify__input:focus{outline:0}.tagify__input:focus::before{transition:.2s ease-out;opacity:0;transform:translatex(6px)}@supports (-ms-ime-align:auto){.tagify__input:focus::before{display:none}}.tagify__input:focus:empty::before{transition:.2s ease-out;opacity:1;transform:none;color:rgba(0,0,0,.25);color:var(--placeholder-color-focus)}@-moz-document url-prefix(){.tagify__input:focus:empty::after{display:none}}.tagify__input::before{content:attr(data-placeholder);height:1em;line-height:1em;margin:auto 0;z-index:1;color:var(--placeholder-color);white-space:nowrap;pointer-events:none;opacity:0;position:absolute}.tagify__input::after{content:attr(data-suggest);display:inline-block;vertical-align:middle;position:absolute;min-width:calc(100% - 1.5em);text-overflow:ellipsis;overflow:hidden;white-space:pre;color:var(--tag-text-color);opacity:.3;pointer-events:none;max-width:100px}.tagify__input .tagify__tag{margin:0 1px}.tagify--mix{display:block}.tagify--mix .tagify__input{padding:5px;margin:0;width:100%;height:100%;line-height:1.5;display:block}.tagify--mix .tagify__input::before{height:auto;display:none;line-height:inherit}.tagify--mix .tagify__input::after{content:none}.tagify--select::after{content:">";opacity:.5;position:absolute;top:50%;right:0;bottom:0;font:16px monospace;line-height:8px;height:8px;pointer-events:none;transform:translate(-150%,-50%) scaleX(1.2) rotate(90deg);transition:.2s ease-in-out}.tagify--select[aria-expanded=true]::after{transform:translate(-150%,-50%) rotate(270deg) scaleY(1.2)}.tagify--select .tagify__tag{position:absolute;top:0;right:1.8em;bottom:0}.tagify--select .tagify__tag div{display:none}.tagify--select .tagify__input{width:100%}.tagify--empty .tagify__input::before{transition:.2s ease-out;opacity:1;transform:none;display:inline-block;width:auto}.tagify--mix .tagify--empty .tagify__input::before{display:inline-block}.tagify--focus{--tags-border-color:var(--tags-focus-border-color);transition:0s}.tagify--invalid{--tags-border-color:#D39494}.tagify__dropdown{position:absolute;z-index:9999;transform:translateY(1px);overflow:hidden}.tagify__dropdown[placement=top]{margin-top:0;transform:translateY(-100%)}.tagify__dropdown[placement=top] .tagify__dropdown__wrapper{border-top-width:1.1px;border-bottom-width:0}.tagify__dropdown[position=text]{box-shadow:0 0 0 3px rgba(var(--tagify-dd-color-primary),.1);font-size:.9em}.tagify__dropdown[position=text] .tagify__dropdown__wrapper{border-width:1px}.tagify__dropdown__wrapper{max-height:300px;overflow:auto;overflow-x:hidden;background:var(--tagify-dd-bg-color);border:1px solid;border-color:var(--tagify-dd-color-primary);border-bottom-width:1.5px;border-top-width:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.2);transition:.25s cubic-bezier(0,1,.5,1)}.tagify__dropdown__header:empty{display:none}.tagify__dropdown__footer{display:inline-block;margin-top:.5em;padding:var(--tagify-dd-item-pad);font-size:.7em;font-style:italic;opacity:.5}.tagify__dropdown__footer:empty{display:none}.tagify__dropdown--initial .tagify__dropdown__wrapper{max-height:20px;transform:translateY(-1em)}.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper{transform:translateY(2em)}.tagify__dropdown__item{box-sizing:border-box;padding:var(--tagify-dd-item-pad);margin:1px;white-space:pre-wrap;cursor:pointer;border-radius:2px;position:relative;outline:0;max-height:60px;max-width:100%}.tagify__dropdown__item--active{background:var(--tagify-dd-color-primary);color:#fff}.tagify__dropdown__item:active{filter:brightness(105%)}.tagify__dropdown__item--hidden{padding-top:0;padding-bottom:0;margin:0 1px;pointer-events:none;overflow:hidden;max-height:0;transition:var(--tagify-dd-item--hidden-duration,.3s)!important}.tagify__dropdown__item--hidden>*{transform:translateY(-100%);opacity:0;transition:inherit}
\ No newline at end of file diff --git a/cmd/web/static/js/htmx-head-support.js b/cmd/web/static/js/htmx-head-support.js new file mode 100644 index 0000000..67cfc69 --- /dev/null +++ b/cmd/web/static/js/htmx-head-support.js @@ -0,0 +1,141 @@ +//========================================================== +// head-support.js +// +// An extension to htmx 1.0 to add head tag merging. +//========================================================== +(function(){ + + var api = null; + + function log() { + //console.log(arguments); + } + + function mergeHead(newContent, defaultMergeStrategy) { + + if (newContent && newContent.indexOf('<head') > -1) { + const htmlDoc = document.createElement("html"); + // remove svgs to avoid conflicts + var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); + // extract head tag + var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im); + + // if the head tag exists... + if (headTag) { + + var added = [] + var removed = [] + var preserved = [] + var nodesToAppend = [] + + htmlDoc.innerHTML = headTag; + var newHeadTag = htmlDoc.querySelector("head"); + var currentHead = document.head; + + if (newHeadTag == null) { + return; + } else { + // put all new head elements into a Map, by their outerHTML + var srcToNewHeadNodes = new Map(); + for (const newHeadChild of newHeadTag.children) { + srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); + } + } + + + + // determine merge strategy + var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy; + + // get the current head + for (const currentHeadElt of currentHead.children) { + + // If the current head element is in the map + var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); + var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval"; + var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true"; + if (inNewContent || isPreserved) { + if (isReAppended) { + // remove the current version and let the new version replace it and re-execute + removed.push(currentHeadElt); + } else { + // this element already exists and should not be re-appended, so remove it from + // the new content map, preserving it in the DOM + srcToNewHeadNodes.delete(currentHeadElt.outerHTML); + preserved.push(currentHeadElt); + } + } else { + if (mergeStrategy === "append") { + // we are appending and this existing element is not new content + // so if and only if it is marked for re-append do we do anything + if (isReAppended) { + removed.push(currentHeadElt); + nodesToAppend.push(currentHeadElt); + } + } else { + // if this is a merge, we remove this content since it is not in the new head + if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) { + removed.push(currentHeadElt); + } + } + } + } + + // Push the tremaining new head elements in the Map into the + // nodes to append to the head tag + nodesToAppend.push(...srcToNewHeadNodes.values()); + log("to append: ", nodesToAppend); + + for (const newNode of nodesToAppend) { + log("adding: ", newNode); + var newElt = document.createRange().createContextualFragment(newNode.outerHTML); + log(newElt); + if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) { + currentHead.appendChild(newElt); + added.push(newElt); + } + } + + // remove all removed elements, after we have appended the new elements to avoid + // additional network requests for things like style sheets + for (const removedElement of removed) { + if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) { + currentHead.removeChild(removedElement); + } + } + + api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed}); + } + } + } + + htmx.defineExtension("head-support", { + init: function(apiRef) { + // store a reference to the internal API. + api = apiRef; + + htmx.on('htmx:afterSwap', function(evt){ + var serverResponse = evt.detail.xhr.response; + if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { + mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append"); + } + }) + + htmx.on('htmx:historyRestore', function(evt){ + if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { + if (evt.detail.cacheMiss) { + mergeHead(evt.detail.serverResponse, "merge"); + } else { + mergeHead(evt.detail.item.head, "merge"); + } + } + }) + + htmx.on('htmx:historyItemCreated', function(evt){ + var historyItem = evt.detail.item; + historyItem.head = document.head.outerHTML; + }) + } + }); + +})()
\ No newline at end of file diff --git a/cmd/web/static/js/htmx.js b/cmd/web/static/js/htmx.js new file mode 100644 index 0000000..5ac9041 --- /dev/null +++ b/cmd/web/static/js/htmx.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Y={onLoad:t,process:Pt,on:Z,off:K,trigger:fe,ajax:wr,find:E,findAll:f,closest:v,values:function(e,t){var r=nr(e,t||"post");return r.values},remove:U,addClass:B,removeClass:n,toggleClass:V,takeClass:j,defineExtension:qr,removeExtension:Hr,logAll:X,logNone:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false},parseInterval:d,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Y.config.wsBinaryType;return t},version:"1.9.6"};var r={addTriggerHandler:St,bodyContains:oe,canAccessLocalStorage:M,findThisElement:de,filterValues:lr,hasAttribute:o,getAttributeValue:ee,getClosestAttributeValue:re,getClosestMatch:c,getExpressionVars:xr,getHeaders:sr,getInputValues:nr,getInternalData:ie,getSwapSpecification:fr,getTriggerSpecs:Ze,getTarget:ge,makeFragment:l,mergeObjects:se,makeSettleInfo:T,oobSwap:ye,querySelectorExt:le,selectAndSwap:Fe,settleImmediately:Wt,shouldCancel:tt,triggerEvent:fe,triggerErrorEvent:ue,withExtensions:C};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function Q(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function ee(e,t){return Q(e,t)||Q(e,"data-"+t)}function u(e){return e.parentElement}function te(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function O(e,t,r){var n=ee(t,r);var i=ee(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function re(t,r){var n=null;c(t,function(e){return n=O(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=te().createDocumentFragment()}return i}function H(e){return e.match(/<body/)}function l(e){var t=!H(e);if(Y.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":case"style":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function ne(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ie(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function ae(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function P(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function oe(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return te().body.contains(e.getRootNode().host)}else{return te().body.contains(e)}}function k(e){return e.trim().split(/\s+/)}function se(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){y(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return gr(te().body,function(){return eval(e)})}function t(t){var e=Y.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Y.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function F(){Y.logger=null}function E(e,t){if(t){return e.querySelector(t)}else{return E(te(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(te(),e)}}function U(e,t){e=s(e);if(t){setTimeout(function(){U(e);e=null},t)}else{e.parentElement.removeChild(e)}}function B(e,t,r){e=s(e);if(r){setTimeout(function(){B(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);ae(e.parentElement.children,function(e){n(e,t)});B(e,t)}function v(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[v(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[E(e,z(t.substr(5)))]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[G(e,z(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return te().querySelectorAll(z(t))}}var $=function(e,t){var r=te().querySelectorAll(t);for(var n=0;n<r.length;n++){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return i}}};var G=function(e,t){var r=te().querySelectorAll(t);for(var n=r.length-1;n>=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function le(e,t){if(t){return W(e,t)[0]}else{return W(te().body,e)[0]}}function s(e){if(L(e,"String")){return E(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:te().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Nr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Nr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var he=te().createElement("output");function ve(e,t){var r=re(e,t);if(r){if(r==="this"){return[de(e,t)]}else{var n=W(e,r);if(n.length===0){y('The selector "'+r+'" on '+t+" returned no matches!");return[he]}else{return n}}}}function de(e,t){return c(e,function(e){return ee(e,t)!=null})}function ge(e){var t=re(e,"hx-target");if(t){if(t==="this"){return de(e,"hx-target")}else{return le(e,t)}}else{var r=ie(e);if(r.boosted){return te().body}else{return e}}}function me(e){var t=Y.config.attributesToSettle;for(var r=0;r<t.length;r++){if(e===t[r]){return true}}return false}function pe(t,r){ae(t.attributes,function(e){if(!r.hasAttribute(e.name)&&me(e.name)){t.removeAttribute(e.name)}});ae(r.attributes,function(e){if(me(e.name)){t.setAttribute(e.name,e.value)}})}function xe(e,t){var r=Lr(t);for(var n=0;n<r.length;n++){var i=r[n];try{if(i.isInlineSwap(e)){return true}}catch(e){y(e)}}return e==="outerHTML"}function ye(e,i,a){var t="#"+Q(i,"id");var o="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=te().querySelectorAll(t);if(r){ae(r,function(e){var t;var r=i.cloneNode(true);t=te().createDocumentFragment();t.appendChild(r);if(!xe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!fe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){De(o,e,e,t,a)}ae(a.elts,function(e){fe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ue(te().body,"htmx:oobErrorNoTarget",{content:i})}return e}function be(e,t,r){var n=re(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e<i.length;e++){var a=i[e].split(":",2);var o=a[0].trim();if(o.indexOf("#")===0){o=o.substring(1)}var s=a[1]||"true";var l=t.querySelector("#"+o);if(l){ye(s,l,r)}}}ae(f(t,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=ee(e,"hx-swap-oob");if(t!=null){ye(t,e,r)}})}function we(e){ae(f(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=ee(e,"id");var r=te().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function Se(o,e,s){ae(e.querySelectorAll("[id]"),function(e){var t=Q(e,"id");if(t&&t.length>0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Y.config.addedClass);Pt(e);Ct(e);Ce(e);fe(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;B(i,Y.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r<e.length){t=(t<<5)-t+e.charCodeAt(r++)|0}return t}function Re(e){var t=0;if(e.attributes){for(var r=0;r<e.attributes.length;r++){var n=e.attributes[r];if(n.value){t=Te(n.name,t);t=Te(n.value,t)}}}return t}function Oe(t){var r=ie(t);if(r.onHandlers){for(let e=0;e<r.onHandlers.length;e++){const n=r.onHandlers[e];t.removeEventListener(n.event,n.listener)}delete r.onHandlers}}function qe(e){var t=ie(e);if(t.timeout){clearTimeout(t.timeout)}if(t.webSocket){t.webSocket.close()}if(t.sseEventSource){t.sseEventSource.close()}if(t.listenerInfos){ae(t.listenerInfos,function(e){if(e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.initHash){t.initHash=null}Oe(e)}function m(e){fe(e,"htmx:beforeCleanupElement");qe(e);if(e.children){ae(e.children,function(e){m(e)})}}function He(t,e,r){if(t.tagName==="BODY"){return ke(t,e,r)}else{var n;var i=t.previousSibling;a(u(t),t,e,r);if(i==null){n=u(t).firstChild}else{n=i.nextSibling}ie(t).replacedWith=n;r.elts=r.elts.filter(function(e){return e!=t});while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}m(t);u(t).removeChild(t)}}function Le(e,t,r){return a(e,e.firstChild,t,r)}function Ae(e,t,r){return a(u(e),e,t,r)}function Ne(e,t,r){return a(e,null,t,r)}function Ie(e,t,r){return a(u(e),e.nextSibling,t,r)}function Pe(e,t,r){m(e);return u(e).removeChild(e)}function ke(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){m(n.nextSibling);e.removeChild(n.nextSibling)}m(n);e.removeChild(n)}}function Me(e,t,r){var n=r||re(e,"hx-select");if(n){var i=te().createDocumentFragment();ae(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function De(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":He(r,n,i);return;case"afterbegin":Le(r,n,i);return;case"beforebegin":Ae(r,n,i);return;case"beforeend":Ne(r,n,i);return;case"afterend":Ie(r,n,i);return;case"delete":Pe(r,n,i);return;default:var a=Lr(t);for(var o=0;o<a.length;o++){var s=a[o];try{var l=s.handleSwap(e,r,n,i);if(l){if(typeof l.length!=="undefined"){for(var u=0;u<l.length;u++){var f=l[u];if(f.nodeType!==Node.TEXT_NODE&&f.nodeType!==Node.COMMENT_NODE){i.tasks.push(Ee(f))}}}return}}catch(e){y(e)}}if(e==="innerHTML"){ke(r,n,i)}else{De(Y.config.defaultSwapStyle,t,r,n,i)}}}function Xe(e){if(e.indexOf("<title")>-1){var t=e.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Fe(e,t,r,n,i,a){i.title=Xe(n);var o=l(n);if(o){be(r,o,i);o=Me(r,o,a);we(o);return De(e,r,t,o,i)}}function Ue(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=S(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!N(o)){o={value:o}}fe(r,a,o)}}}else{var s=n.split(",");for(var l=0;l<s.length;l++){fe(r,s[l].trim(),[])}}}var Be=/\s/;var p=/[\s,]/;var Ve=/[_$a-zA-Z]/;var je=/[_$a-zA-Z0-9]/;var _e=['"',"'","/"];var ze=/[^\s]/;function We(e){var t=[];var r=0;while(r<e.length){if(Ve.exec(e.charAt(r))){var n=r;while(je.exec(e.charAt(r+1))){r++}t.push(e.substr(n,r-n+1))}else if(_e.indexOf(e.charAt(r))!==-1){var i=e.charAt(r);var n=r;r++;while(r<e.length&&e.charAt(r)!==i){if(e.charAt(r)==="\\"){r++}r++}t.push(e.substr(n,r-n+1))}else{var a=e.charAt(r);t.push(a)}r++}return t}function $e(e,t,r){return Ve.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function Ge(e,t,r){if(t[0]==="["){t.shift();var n=1;var i=" return (function("+r+"){ return (";var a=null;while(t.length>0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=gr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ue(te().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if($e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function x(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Je="input, textarea, select";function Ze(e){var t=ee(e,"hx-trigger");var r=[];if(t){var n=We(t);do{x(n,ze);var i=n.length;var a=x(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};x(n,ze);o.pollInterval=d(x(n,/[,\[\s]/));x(n,ze);var s=Ge(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=Ge(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){x(n,ze);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=d(x(n,p))}else if(u==="from"&&n[0]===":"){n.shift();var f=x(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();f+=" "+x(n,p)}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=x(n,p)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=d(x(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=x(n,p)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=x(n,p)}else{ue(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ue(e,"htmx:syntax:error",{token:n.shift()})}x(n,ze)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,Je)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Ke(e){ie(e).cancelled=true}function Ye(e,t,r){var n=ie(e);n.timeout=setTimeout(function(){if(oe(e)&&n.cancelled!==true){if(!nt(r,e,Mt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ye(e,t,r)}},r.pollInterval)}function Qe(e){return location.hostname===e.hostname&&Q(e,"href")&&Q(e,"href").indexOf("#")!==0}function et(t,r,e){if(t.tagName==="A"&&Qe(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=Q(t,"href")}else{var a=Q(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=Q(t,"action")}e.forEach(function(e){it(t,function(e,t){if(v(e,Y.config.disableSelector)){m(e);return}ce(n,i,e,t)},r,e,true)})}}function tt(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function rt(e,t){return ie(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function nt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ue(te().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function it(a,o,e,s,l){var u=ie(a);var t;if(s.from){t=W(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ie(e);t.lastValue=e.value})}ae(t,function(n){var i=function(e){if(!oe(a)){n.removeEventListener(s.trigger,i);return}if(rt(a,e)){return}if(l||tt(e,a)){e.preventDefault()}if(nt(s,a,e)){return}var t=ie(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ie(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{fe(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var at=false;var ot=null;function st(){if(!ot){ot=function(){at=true};window.addEventListener("scroll",ot);setInterval(function(){if(at){at=false;ae(te().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){lt(e)})}},200)}}function lt(t){if(!o(t,"data-hx-revealed")&&P(t)){t.setAttribute("data-hx-revealed","true");var e=ie(t);if(e.initHash){fe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){fe(t,"revealed")},{once:true})}}}function ut(e,t,r){var n=k(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){ft(e,a[1],0)}if(a[0]==="send"){ht(e)}}}function ft(s,r,n){if(!oe(s)){return}if(r.indexOf("/")==0){var e=location.hostname+(location.port?":"+location.port:"");if(location.protocol=="https:"){r="wss://"+e+r}else if(location.protocol=="http:"){r="ws://"+e+r}}var t=Y.createWebSocket(r);t.onerror=function(e){ue(s,"htmx:wsError",{error:e,socket:t});ct(s)};t.onclose=function(e){if([1006,1012,1013].indexOf(e.code)>=0){var t=vt(n);setTimeout(function(){ft(s,r,n+1)},t)}};t.onopen=function(e){n=0};ie(s).webSocket=t;t.addEventListener("message",function(e){if(ct(s)){return}var t=e.data;C(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=I(n.children);for(var a=0;a<i.length;a++){var o=i[a];ye(ee(o,"hx-swap-oob")||"true",o,r)}Wt(r.tasks)})}function ct(e){if(!oe(e)){ie(e).webSocket.close();return true}}function ht(u){var f=c(u,function(e){return ie(e).webSocket!=null});if(f){u.addEventListener(Ze(u)[0].trigger,function(e){var t=ie(f).webSocket;var r=sr(u,f);var n=nr(u,"post");var i=n.errors;var a=n.values;var o=xr(u);var s=se(a,o);var l=lr(s,u);l["HEADERS"]=r;if(i&&i.length>0){fe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(tt(e,u)){e.preventDefault()}})}else{ue(u,"htmx:noWebSocketSourceError")}}function vt(e){var t=Y.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}y('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function dt(e,t,r){var n=k(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){gt(e,a[1])}if(a[0]==="swap"){mt(e,a[1])}}}function gt(t,e){var r=Y.createEventSource(e);r.onerror=function(e){ue(t,"htmx:sseError",{error:e,source:r});xt(t)};ie(t).sseEventSource=r}function mt(a,o){var s=c(a,yt);if(s){var l=ie(s).sseEventSource;var u=function(e){if(xt(s)){return}if(!oe(a)){l.removeEventListener(o,u);return}var t=e.data;C(a,function(e){t=e.transformResponse(t,null,a)});var r=fr(a);var n=ge(a);var i=T(a);Fe(r.swapStyle,n,a,t,i);Wt(i.tasks);fe(a,"htmx:sseMessage",e)};ie(a).sseListener=u;l.addEventListener(o,u)}else{ue(a,"htmx:noSSESourceError")}}function pt(e,t,r){var n=c(e,yt);if(n){var i=ie(n).sseEventSource;var a=function(){if(!xt(n)){if(oe(e)){t(e)}else{i.removeEventListener(r,a)}}};ie(e).sseListener=a;i.addEventListener(r,a)}else{ue(e,"htmx:noSSESourceError")}}function xt(e){if(!oe(e)){ie(e).sseEventSource.close();return true}}function yt(e){return ie(e).sseEventSource!=null}function bt(e,t,r,n){var i=function(){if(!r.loaded){r.loaded=true;t(e)}};if(n){setTimeout(i,n)}else{i()}}function wt(t,i,e){var a=false;ae(b,function(r){if(o(t,"hx-"+r)){var n=ee(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){St(t,e,i,function(e,t){if(v(e,Y.config.disableSelector)){m(e);return}ce(r,n,e,t)})})}});return a}function St(n,e,t,r){if(e.sseEvent){pt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){st();it(n,r,t,e);lt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=le(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++){var r=e[t];if(r.isIntersecting){fe(n,"intersect");break}}},i);a.observe(n);it(n,r,t,e)}else if(e.trigger==="load"){if(!nt(e,n,Mt("load",{elt:n}))){bt(n,r,t,e.delay)}}else if(e.pollInterval){t.polling=true;Ye(n,r,e)}else{it(n,r,t,e)}}function Et(e){if(Y.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=te().createElement("script");ae(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Y.config.inlineScriptNonce){t.nonce=Y.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){y(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Ct(e){if(h(e,"script")){Et(e)}ae(f(e,"script"),function(e){Et(e)})}function Tt(){return document.querySelector("[hx-boost], [data-hx-boost]")}function Rt(e){var t=null;var r=[];if(document.evaluate){var n=document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]',e);while(t=n.iterateNext())r.push(t)}else{var i=document.getElementsByTagName("*");for(var a=0;a<i.length;a++){var o=i[a].attributes;for(var s=0;s<o.length;s++){var l=o[s].name;if(g(l,"hx-on:")||g(l,"data-hx-on:")){r.push(i[a])}}}}return r}function Ot(e){if(e.querySelectorAll){var t=Tt()?", a":"";var r=e.querySelectorAll(w+t+", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws],"+" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");return r}else{return[]}}function qt(e){var n=s("#"+Q(e,"form"))||v(e,"form");if(!n){return}var t=function(e){var t=v(e.target,"button, input[type='submit']");if(t!==null){var r=ie(n);r.lastButtonClicked=t}};e.addEventListener("click",t);e.addEventListener("focusin",t);e.addEventListener("focusout",function(e){var t=ie(n);t.lastButtonClicked=null})}function Ht(e){var t=We(e);var r=0;for(let e=0;e<t.length;e++){const n=t[e];if(n==="{"){r++}else if(n==="}"){r--}}return r}function Lt(t,e,r){var n=ie(t);n.onHandlers=[];var i;var a=function(e){return gr(t,function(){if(!i){i=new Function("event",r)}i.call(t,e)})};t.addEventListener(e,a);n.onHandlers.push({event:e,listener:a})}function At(e){var t=ee(e,"hx-on");if(t){var r={};var n=t.split("\n");var i=null;var a=0;while(n.length>0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Ht(o)}for(var l in r){Lt(e,l,r[l])}}}function Nt(t){Oe(t);for(var e=0;e<t.attributes.length;e++){var r=t.attributes[e].name;var n=t.attributes[e].value;if(g(r,"hx-on:")||g(r,"data-hx-on:")){let e=r.slice(r.indexOf(":")+1);if(g(e,":"))e="htmx"+e;Lt(t,e,n)}}}function It(t){if(v(t,Y.config.disableSelector)){m(t);return}var r=ie(t);if(r.initHash!==Re(t)){qe(t);r.initHash=Re(t);At(t);fe(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=Ze(t);var n=wt(t,r,e);if(!n){if(re(t,"hx-boost")==="true"){et(t,r,e)}else if(o(t,"hx-trigger")){e.forEach(function(e){St(t,e,r,function(){})})}}if(t.tagName==="FORM"||Q(t,"type")==="submit"&&o(t,"form")){qt(t)}var i=ee(t,"hx-sse");if(i){dt(t,r,i)}var a=ee(t,"hx-ws");if(a){ut(t,r,a)}fe(t,"htmx:afterProcessNode")}}function Pt(e){e=s(e);if(v(e,Y.config.disableSelector)){m(e);return}It(e);ae(Ot(e),function(e){It(e)});ae(Rt(e),Nt)}function kt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Mt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=te().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function ue(e,t,r){fe(e,t,se({error:t},r))}function Dt(e){return e==="htmx:afterProcessNode"}function C(e,t){ae(Lr(e),function(e){try{t(e)}catch(e){y(e)}})}function y(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function fe(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=Mt(t,r);if(Y.logger&&!Dt(t)){Y.logger(e,t,r)}if(r.error){y(r.error);fe(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=kt(t);if(i&&a!==t){var o=Mt(a,n.detail);i=i&&e.dispatchEvent(o)}C(e,function(e){i=i&&(e.onEvent(t,n)!==false&&!n.defaultPrevented)});return i}var Xt=location.pathname+location.search;function Ft(){var e=te().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||te().body}function Ut(e,t,r,n){if(!M()){return}e=D(e);var i=S(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;a<i.length;a++){if(i[a].url===e){i.splice(a,1);break}}var o={url:e,content:t,title:r,scroll:n};fe(te().body,"htmx:historyItemCreated",{item:o,cache:i});i.push(o);while(i.length>Y.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ue(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Bt(e){if(!M()){return null}e=D(e);var t=S(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r<t.length;r++){if(t[r].url===e){return t[r]}}return null}function Vt(e){var t=Y.config.requestClass;var r=e.cloneNode(true);ae(f(r,"."+t),function(e){n(e,t)});return r.innerHTML}function jt(){var e=Ft();var t=Xt||location.pathname+location.search;var r;try{r=te().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){r=te().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!r){fe(te().body,"htmx:beforeHistorySave",{path:t,historyElt:e});Ut(t,Vt(e),te().title,window.scrollY)}if(Y.config.historyEnabled)history.replaceState({htmx:true},te().title,window.location.href)}function _t(e){if(Y.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(_(e,"&")||_(e,"?")){e=e.slice(0,-1)}}if(Y.config.historyEnabled){history.pushState({htmx:true},"",e)}Xt=e}function zt(e){if(Y.config.historyEnabled)history.replaceState({htmx:true},"",e);Xt=e}function Wt(e){ae(e,function(e){e.call()})}function $t(a){var e=new XMLHttpRequest;var o={path:a,xhr:e};fe(te().body,"htmx:historyCacheMiss",o);e.open("GET",a,true);e.setRequestHeader("HX-History-Restore-Request","true");e.onload=function(){if(this.status>=200&&this.status<400){fe(te().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Ft();var r=T(t);var n=Xe(this.response);if(n){var i=E("title");if(i){i.innerHTML=n}else{window.document.title=n}}ke(t,e,r);Wt(r.tasks);Xt=a;fe(te().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ue(te().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function Gt(e){jt();e=e||location.pathname+location.search;var t=Bt(e);if(t){var r=l(t.content);var n=Ft();var i=T(n);ke(n,r,i);Wt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Xt=e;fe(te().body,"htmx:historyRestore",{path:e,item:t})}else{if(Y.config.refreshOnHistoryMiss){window.location.reload(true)}else{$t(e)}}}function Jt(e){var t=ve(e,"hx-indicator");if(t==null){t=[e]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Y.config.requestClass)});return t}function Zt(e){var t=ve(e,"hx-disabled-elt");if(t==null){t=[]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function Kt(e,t){ae(e,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Y.config.requestClass)}});ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function Yt(e,t){for(var r=0;r<e.length;r++){var n=e[r];if(n.isSameNode(t)){return true}}return false}function Qt(e){if(e.name===""||e.name==null||e.disabled){return false}if(e.type==="button"||e.type==="submit"||e.tagName==="image"||e.tagName==="reset"||e.tagName==="file"){return false}if(e.type==="checkbox"||e.type==="radio"){return e.checked}return true}function er(e,t,r){if(e!=null&&t!=null){var n=r[e];if(n===undefined){r[e]=t}else if(Array.isArray(n)){if(Array.isArray(t)){r[e]=n.concat(t)}else{n.push(t)}}else{if(Array.isArray(t)){r[e]=[n].concat(t)}else{r[e]=[n,t]}}}}function tr(t,r,n,e,i){if(e==null||Yt(t,e)){return}else{t.push(e)}if(Qt(e)){var a=Q(e,"name");var o=e.value;if(e.multiple){o=I(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e.files){o=I(e.files)}er(a,o,r);if(i){rr(e,n)}}if(h(e,"form")){var s=e.elements;ae(s,function(e){tr(t,r,n,e,i)})}}function rr(e,t){if(e.willValidate){fe(e,"htmx:validation:validate");if(!e.checkValidity()){t.push({elt:e,message:e.validationMessage,validity:e.validity});fe(e,"htmx:validation:failed",{message:e.validationMessage,validity:e.validity})}}}function nr(e,t){var r=[];var n={};var i={};var a=[];var o=ie(e);var s=h(e,"form")&&e.noValidate!==true||ee(e,"hx-validate")==="true";if(o.lastButtonClicked){s=s&&o.lastButtonClicked.formNoValidate!==true}if(t!=="get"){tr(r,i,a,v(e,"form"),s)}tr(r,n,a,e,s);if(o.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&Q(e,"type")==="submit"){var l=o.lastButtonClicked||e;var u=Q(l,"name");er(u,l.value,i)}var f=ve(e,"hx-include");ae(f,function(e){tr(r,n,a,e,s);if(!h(e,"form")){ae(e.querySelectorAll(Je),function(e){tr(r,n,a,e,s)})}});n=se(n,i);return{errors:a,values:n}}function ir(e,t,r){if(e!==""){e+="&"}if(String(r)==="[object Object]"){r=JSON.stringify(r)}var n=encodeURIComponent(r);e+=encodeURIComponent(t)+"="+n;return e}function ar(e){var t="";for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){ae(n,function(e){t=ir(t,r,e)})}else{t=ir(t,r,n)}}}return t}function or(e){var t=new FormData;for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){ae(n,function(e){t.append(r,e)})}else{t.append(r,n)}}}return t}function sr(e,t,r){var n={"HX-Request":"true","HX-Trigger":Q(e,"id"),"HX-Trigger-Name":Q(e,"name"),"HX-Target":ee(t,"id"),"HX-Current-URL":te().location.href};dr(e,"hx-headers",false,n);if(r!==undefined){n["HX-Prompt"]=r}if(ie(e).boosted){n["HX-Boosted"]="true"}return n}function lr(t,e){var r=re(e,"hx-params");if(r){if(r==="none"){return{}}else if(r==="*"){return t}else if(r.indexOf("not ")===0){ae(r.substr(4).split(","),function(e){e=e.trim();delete t[e]});return t}else{var n={};ae(r.split(","),function(e){e=e.trim();n[e]=t[e]});return n}}else{return t}}function ur(e){return Q(e,"href")&&Q(e,"href").indexOf("#")>=0}function fr(e,t){var r=t?t:re(e,"hx-swap");var n={swapStyle:ie(e).boosted?"innerHTML":Y.config.defaultSwapStyle,swapDelay:Y.config.defaultSwapDelay,settleDelay:Y.config.defaultSettleDelay};if(ie(e).boosted&&!ur(e)){n["show"]="top"}if(r){var i=k(r);if(i.length>0){for(var a=0;a<i.length;a++){var o=i[a];if(o.indexOf("swap:")===0){n["swapDelay"]=d(o.substr(5))}else if(o.indexOf("settle:")===0){n["settleDelay"]=d(o.substr(7))}else if(o.indexOf("transition:")===0){n["transition"]=o.substr(11)==="true"}else if(o.indexOf("ignoreTitle:")===0){n["ignoreTitle"]=o.substr(12)==="true"}else if(o.indexOf("scroll:")===0){var s=o.substr(7);var l=s.split(":");var u=l.pop();var f=l.length>0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{y("Unknown modifier in hx-swap: "+o)}}}}return n}function cr(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&Q(e,"enctype")==="multipart/form-data"}function hr(t,r,n){var i=null;C(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(cr(r)){return or(n)}else{return ar(n)}}}function T(e){return{tasks:[],elts:[e]}}function vr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=le(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=le(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Y.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Y.config.scrollBehavior})}}}function dr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=ee(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=gr(e,function(){return Function("return ("+a+")")()},{})}else{s=S(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return dr(u(e),t,r,n)}function gr(e,t,r){if(Y.config.allowEval){return t()}else{ue(e,"htmx:evalDisallowedError");return r}}function mr(e,t){return dr(e,"hx-vars",true,t)}function pr(e,t){return dr(e,"hx-vals",false,t)}function xr(e){return se(mr(e),pr(e))}function yr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function br(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ue(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return e.getAllResponseHeaders().match(t)}function wr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return ce(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return ce(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return ce(e,t,null,null,{returnPromise:true})}}function Sr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Er(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Y.config.selfRequestsOnly){if(!n){return false}}return fe(e,"htmx:validateUrl",se({url:i,sameHost:n},r))}function ce(e,t,n,r,i,M){var a=null;var o=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var s=new Promise(function(e,t){a=e;o=t})}if(n==null){n=te().body}var D=i.handler||Tr;if(!oe(n)){ne(a);return s}var l=i.targetOverride||ge(n);if(l==null||l==he){ue(n,"htmx:targetError",{target:ee(n,"hx-target")});ne(o);return s}var u=ie(n);var f=u.lastButtonClicked;if(f){var c=Q(f,"formaction");if(c!=null){t=c}var h=Q(f,"formmethod");if(h!=null){e=h}}if(!M){var X=function(){return ce(e,t,n,r,i,true)};var F={target:l,elt:n,path:t,verb:e,triggeringEvent:r,etc:i,issueRequest:X};if(fe(n,"htmx:confirm",F)===false){ne(a);return s}}var v=n;var d=re(n,"hx-sync");var g=null;var m=false;if(d){var p=d.split(":");var x=p[0].trim();if(x==="this"){v=de(n,"hx-sync")}else{v=le(n,x)}d=(p[1]||"drop").trim();u=ie(v);if(d==="drop"&&u.xhr&&u.abortable!==true){ne(a);return s}else if(d==="abort"){if(u.xhr){ne(a);return s}else{m=true}}else if(d==="replace"){fe(v,"htmx:abort")}else if(d.indexOf("queue")===0){var U=d.split(" ");g=(U[1]||"last").trim()}}if(u.xhr){if(u.abortable){fe(v,"htmx:abort")}else{if(g==null){if(r){var y=ie(r);if(y&&y.triggerSpec&&y.triggerSpec.queue){g=y.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){ce(e,t,n,r,i)})}else if(g==="all"){u.queuedRequests.push(function(){ce(e,t,n,r,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){ce(e,t,n,r,i)})}ne(a);return s}}var b=new XMLHttpRequest;u.xhr=b;u.abortable=m;var w=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){var e=u.queuedRequests.shift();e()}};var B=re(n,"hx-prompt");if(B){var S=prompt(B);if(S===null||!fe(n,"htmx:prompt",{prompt:S,target:l})){ne(a);w();return s}}var V=re(n,"hx-confirm");if(V){if(!confirm(V)){ne(a);w();return s}}var E=sr(n,l,S);if(i.headers){E=se(E,i.headers)}var j=nr(n,e);var C=j.errors;var T=j.values;if(i.values){T=se(T,i.values)}var _=xr(n);var z=se(T,_);var R=lr(z,n);if(e!=="get"&&!cr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(Y.config.getCacheBusterParam&&e==="get"){R["org.htmx.cache-buster"]=Q(l,"id")||"true"}if(t==null||t===""){t=te().location.href}var O=dr(n,"hx-request");var W=ie(n).boosted;var q=Y.config.methodsThatUseUrlParams.indexOf(e)>=0;var H={boosted:W,useUrlParams:q,parameters:R,unfilteredParameters:z,headers:E,target:l,verb:e,errors:C,withCredentials:i.credentials||O.credentials||Y.config.withCredentials,timeout:i.timeout||O.timeout||Y.config.timeout,path:t,triggeringEvent:r};if(!fe(n,"htmx:configRequest",H)){ne(a);w();return s}t=H.path;e=H.verb;E=H.headers;R=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){fe(n,"htmx:validation:halted",H);ne(a);w();return s}var $=t.split("#");var G=$[0];var L=$[1];var A=t;if(q){A=G;var J=Object.keys(R).length!==0;if(J){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=ar(R);if(L){A+="#"+L}}}if(!Er(n,A,H)){ue(n,"htmx:invalidPath",H);ne(o);return s}b.open(e.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var Z=E[N];yr(b,N,Z)}}}var I={xhr:b,target:l,requestConfig:H,etc:i,boosted:W,pathInfo:{requestPath:t,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Sr(n);I.pathInfo.responsePath=br(b);D(n,I);Kt(P,k);fe(n,"htmx:afterRequest",I);fe(n,"htmx:afterOnLoad",I);if(!oe(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(oe(r)){t=r}}if(t){fe(t,"htmx:afterRequest",I);fe(t,"htmx:afterOnLoad",I)}}ne(a);w()}catch(e){ue(n,"htmx:onLoadError",se({error:e},I));throw e}};b.onerror=function(){Kt(P,k);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendError",I);ne(o);w()};b.onabort=function(){Kt(P,k);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendAbort",I);ne(o);w()};b.ontimeout=function(){Kt(P,k);ue(n,"htmx:afterRequest",I);ue(n,"htmx:timeout",I);ne(o);w()};if(!fe(n,"htmx:beforeRequest",I)){ne(a);w();return s}var P=Jt(n);var k=Zt(n);ae(["loadstart","loadend","progress","abort"],function(t){ae([b,b.upload],function(e){e.addEventListener(t,function(e){fe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});fe(n,"htmx:beforeSend",I);var K=q?null:hr(b,n,R);b.send(K);return s}function Cr(e,t){var r=t.xhr;var n=null;var i=null;if(R(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(R(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(R(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=re(e,"hx-push-url");var l=re(e,"hx-replace-url");var u=ie(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Tr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;if(!fe(l,"htmx:beforeOnLoad",u))return;if(R(f,/HX-Trigger:/i)){Ue(f,"HX-Trigger",l)}if(R(f,/HX-Location:/i)){jt();var r=f.getResponseHeader("HX-Location");var h;if(r.indexOf("{")===0){h=S(r);r=h["path"];delete h["path"]}wr("GET",r,h).then(function(){_t(r)});return}var n=R(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(R(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(R(f,/HX-Retarget:/i)){u.target=te().querySelector(f.getResponseHeader("HX-Retarget"))}var v=Cr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var d=f.response;var a=f.status>=400;var g=Y.config.ignoreTitle;var o=se({shouldSwap:i,serverResponse:d,isError:a,ignoreTitle:g},u);if(!fe(c,"htmx:beforeSwap",o))return;c=o.target;d=o.serverResponse;a=o.isError;g=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){Ke(l)}C(l,function(e){d=e.transformResponse(d,f,l)});if(v.type){jt()}var s=e.swapOverride;if(R(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var h=fr(l,s);if(h.hasOwnProperty("ignoreTitle")){g=h.ignoreTitle}c.classList.add(Y.config.swappingClass);var m=null;var p=null;var x=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(R(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=T(c);Fe(h.swapStyle,c,l,d,n,r);if(t.elt&&!oe(t.elt)&&Q(t.elt,"id")){var i=document.getElementById(Q(t.elt,"id"));var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!Y.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Y.config.swappingClass);ae(n.elts,function(e){if(e.classList){e.classList.add(Y.config.settlingClass)}fe(e,"htmx:afterSwap",u)});if(R(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!oe(l)){o=te().body}Ue(f,"HX-Trigger-After-Swap",o)}var s=function(){ae(n.tasks,function(e){e.call()});ae(n.elts,function(e){if(e.classList){e.classList.remove(Y.config.settlingClass)}fe(e,"htmx:afterSettle",u)});if(v.type){if(v.type==="push"){_t(v.path);fe(te().body,"htmx:pushedIntoHistory",{path:v.path})}else{zt(v.path);fe(te().body,"htmx:replacedInHistory",{path:v.path})}}if(u.pathInfo.anchor){var e=E("#"+u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!g){var t=E("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}vr(n.elts,h);if(R(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!oe(l)){r=te().body}Ue(f,"HX-Trigger-After-Settle",r)}ne(m)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ue(l,"htmx:swapError",u);ne(p);throw e}};var y=Y.config.globalViewTransitions;if(h.hasOwnProperty("transition")){y=h.transition}if(y&&fe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var b=new Promise(function(e,t){m=e;p=t});var w=x;x=function(){document.startViewTransition(function(){w();return b})}}if(h.swapDelay>0){setTimeout(x,h.swapDelay)}else{x()}}if(a){ue(l,"htmx:responseError",se({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Rr={};function Or(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function qr(e,t){if(t.init){t.init(r)}Rr[e]=se(Or(),t)}function Hr(e){delete Rr[e]}function Lr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=ee(e,"hx-ext");if(t){ae(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Rr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Lr(u(e),r,n)}var Ar=false;te().addEventListener("DOMContentLoaded",function(){Ar=true});function Nr(e){if(Ar||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Ir(){if(Y.config.includeIndicatorStyles!==false){te().head.insertAdjacentHTML("beforeend","<style> ."+Y.config.indicatorClass+"{opacity:0;transition: opacity 200ms ease-in;} ."+Y.config.requestClass+" ."+Y.config.indicatorClass+"{opacity:1} ."+Y.config.requestClass+"."+Y.config.indicatorClass+"{opacity:1} </style>")}}function Pr(){var e=te().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function kr(){var e=Pr();if(e){Y.config=se(Y.config,e)}}Nr(function(){kr();Ir();var e=te().body;Pt(e);var t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ie(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){Gt();ae(t,function(e){fe(e,"htmx:restored",{document:te(),triggerEvent:fe})})}else{if(r){r(e)}}};setTimeout(function(){fe(e,"htmx:load",{});e=null},0)});return Y}()});
\ No newline at end of file diff --git a/cmd/web/static/js/tagify.js b/cmd/web/static/js/tagify.js new file mode 100644 index 0000000..0ddc485 --- /dev/null +++ b/cmd/web/static/js/tagify.js @@ -0,0 +1,26 @@ +/** + * Tagify (v 4.17.9) - tags input component + * By undefined + * https://github.com/yairEO/tagify + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD. + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s<arguments.length;s++){var a=null!=arguments[s]?arguments[s]:{};s%2?t(Object(a),!0).forEach((function(t){i(e,t,a[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(a)):t(Object(a)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(a,t))}))}return e}function i(t,e,i){return(e=function(t){var e=function(t,e){if("object"!=typeof t||null===t)return t;var i=t[Symbol.toPrimitive];if(void 0!==i){var s=i.call(t,e||"default");if("object"!=typeof s)return s;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(t,"string");return"symbol"==typeof e?e:String(e)}(e))in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}var s="";const a=(t,e,i,s)=>(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase()),n=(t,e)=>t&&Array.isArray(t)&&t.map((t=>o(t,e)));function o(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function r(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function l(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild}function d(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function h(t){return"string"==typeof t?t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/`|'/g,"'"):t}function g(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function p(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(g(e[i])){g(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function c(){const t=[],e={};for(let i of arguments)for(let s of i)g(s)?e[s.value]||(t.push(s),e[s.value]=1):t.includes(s)||t.push(s);return t}function u(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var m=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function v(){return([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}function f(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}function T(t,e){var i=window.getSelection();return e=e||i.getRangeAt(0),"string"==typeof t&&(t=document.createTextNode(t)),e&&(e.deleteContents(),e.insertNode(t)),t}function w(t,e,i){return t?(e&&(t.__tagifyTagData=i?e:p({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag element doesn't exist",t,e),e)}function b(t){if(t&&t.parentNode){var e=t,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}}function y(t,e){t.forEach((t=>{if(w(t.previousSibling)||!t.previousSibling){var i=document.createTextNode(s);t.before(i),e&&b(i)}}))}var x={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,onChangeAfterBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,createInvalidTags:!0,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",scopeLoading:"tagify--loading",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty",inputInvalid:"tagify__input--invalid",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownHeader:"tagify__dropdown__header",dropdownFooter:"tagify__dropdown__footer",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownItemHidden:"tagify__dropdown__item--hidden",dropdownInital:"tagify__dropdown--initial",tag:"tagify__tag",tagText:"tagify__tag-text",tagX:"tagify__tag__removeBtn",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,includeSelectedTags:!1,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}};function O(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.dropdown.refs()}var D={refs(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-wrapper']")},getHeaderRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-header']")},getFooterRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-footer']")},getAllSuggestionsRefs(){return[...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]},show(t){var e,i,s,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,l="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,!(r&&!o&&!n.templates.dropdownItemNoMatch||!1===n.dropdown.enable||this.state.isLoading||this.settings.readonly)){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(s=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!s){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!a(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(g(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(s),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(n.classNames.dropdownItemSelector)),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),l||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return"number"==typeof a.dropdown.enabled&&a.dropdown.enabled>=0?(this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position(s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this):this},fill(t){t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems);var e,i=this.settings.templates.dropdownContent.call(this,t);this.DOM.dropdown.content.innerHTML=(e=i)?e.replace(/\>[\r\n ]+\</g,"><").split(/>\s+</).join("><").trim():""},fillHeaderFooter(){var t=this.dropdown.filterListItems(this.state.dropdown.query),e=this.parseTemplate("dropdownHeader",[t]),i=this.parseTemplate("dropdownFooter",[t]),s=this.dropdown.getHeaderRef(),a=this.dropdown.getFooterRef();e&&s?.parentNode.replaceChild(e,s),i&&a?.parentNode.replaceChild(i,a)},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=e.appendTarget===document.body,g=h?window.pageYOffset:e.appendTarget.scrollTop,p=document.fullscreenElement||document.webkitFullscreenElement||document.documentElement,c=p.clientHeight,u=Math.max(p.clientWidth||0,window.innerWidth||0)>480?e.position:"all",m=this.DOM["input"==u?"input":"scope"];if(t=t||l.clientHeight,this.state.dropdown.visible){if("text"==u?(a=(i=function(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}}()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t&&t!=p;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(e.appendTarget),s=(i=m.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),!h){let t=function(){for(var t=0,i=e.appendTarget.parentNode;i;)t+=i.scrollTop||0,i=i.parentNode;return t}();s+=t,a+=t}s=Math.floor(s),a=Math.ceil(a),d=void 0===d?c-i.bottom<t:d,l.style.cssText="left:"+(n+window.pageXOffset)+"px; width:"+o+";"+(d?"top: "+(s+g)+"px":"top: "+(a+g)+"px"),l.setAttribute("placement",d?"top":"bottom"),l.setAttribute("position",u)}}},events:{binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this,null),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(document[s]("scroll",i.position,!0),window[s]("resize",i.position),window[s]("keydown",i.onKeyDown)),this.DOM.dropdown[s]("mouseover",i.onMouseOver),this.DOM.dropdown[s]("mouseleave",i.onMouseLeave),this.DOM.dropdown[s]("mousedown",i.onClick),this.DOM.dropdown.content[s]("scroll",i.onScroll)},callbacks:{onKeyDown(t){if(this.state.hasFocus&&!this.state.composing){var e=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownItemActiveSelector),i=this.dropdown.getSuggestionDataByNode(e);switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault();var s=this.dropdown.getAllSuggestionsRefs(),a="ArrowUp"==t.key||"Up"==t.key;e&&(e=this.dropdown.getNextOrPrevOption(e,!a)),e&&e.matches(this.settings.classNames.dropdownItemSelector)||(e=s[a?s.length-1:0]),this.dropdown.highlightOption(e,!0);break;case"Escape":case"Esc":this.dropdown.hide();break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":if("mix"!=this.settings.mode&&e&&!this.settings.autoComplete.rightKey&&!this.state.editing){t.preventDefault();var n=this.dropdown.getMappedValue(i);return this.input.autocomplete.set.call(this,n),!1}return!0;case"Enter":t.preventDefault(),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{if(e)return this.dropdown.selectOption(e),e=this.dropdown.getNextOrPrevOption(e,!a),void this.dropdown.highlightOption(e);this.dropdown.hide(),"mix"!=this.settings.mode&&this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e,t):this.dropdown.hide()})).catch((t=>console.warn(t)))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t&&t.getAttribute("value");return this.suggestedListItems.find((t=>t.value==e))||null},getNextOrPrevOption(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.dropdown.getAllSuggestionsRefs(),s=i.findIndex((e=>e===t));return e?i[s+1]:i[s-1]},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.dropdown.getSuggestionDataByNode(t),this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t,e){var i=this.settings.dropdown,s=i.clearOnSelect,a=i.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(a&&this.dropdown.hide());e=e||{};var n=t.getAttribute("value"),o="noMatch"==n,r=this.suggestedListItems.find((t=>(t.value??t)==n));this.trigger("dropdown:select",{data:r,elm:t,event:e}),n&&(r||o)?(this.state.editing?this.onEditTagDone(null,p({__isValid:!0},this.normalizeTags([r])[0])):this["mix"==this.settings.mode?"addMixTags":"addTags"]([r||this.input.raw.call(this)],s),this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),a&&setTimeout(this.dropdown.hide.bind(this)),t.addEventListener("transitionend",(()=>{this.dropdown.fillHeaderFooter(),setTimeout((()=>t.remove()),100)}),{once:!0}),t.classList.add(this.settings.classNames.dropdownItemHidden))):a&&setTimeout(this.dropdown.hide.bind(this))},selectAll(t){this.suggestedListItems.length=0,this.dropdown.hide(),this.dropdown.filterListItems("");var e=this.dropdown.filterListItems("");return t||(e=this.state.dropdown.suggestions),this.addTags(e,!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,l=r.dropdown,d=(e=e||{},[]),h=[],p=r.whitelist,c=l.maxItems>=0?l.maxItems:1/0,m=l.searchKeys,v=0;if(!(t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t)||!m.length)return d=l.includeSelectedTags?p:p.filter((t=>!this.isTagDuplicate(g(t)?t.value:t))),this.state.dropdown.suggestions=d,d.slice(0,c);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=l.caseSensitive?""+t:(""+t).toLowerCase();v<p.length;v++){let t,r;i=p[v]instanceof Object?p[v]:{value:p[v]};let c=!Object.keys(i).some((t=>m.includes(t)))?["value"]:m;l.fuzzySearch&&!e.exact?(a=c.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),l.accentedSearch&&(a=u(a),o=u(o)),t=0==a.indexOf(o),r=a===o,s=f(a,o)):(t=!0,s=c.some((t=>{var s=""+(i[t]||"");return l.accentedSearch&&(s=u(s),o=u(o)),l.caseSensitive||(s=s.toLowerCase()),r=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!l.includeSelectedTags&&this.isTagDuplicate(g(i)?i.value:i),s&&!n&&(r&&t?h.push(i):"startsWith"==l.sortby&&t?d.unshift(i):d.push(i))}return this.state.dropdown.suggestions=h.concat(d),"function"==typeof l.sortby?l.sortby(h.concat(d),o):h.concat(d).slice(0,c)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return p([],t).map(((t,i)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var s=this.dropdown.getMappedValue(t);return s="string"==typeof s?h(s):s,this.settings.templates.dropdownItem.apply(this,[e(e({},t),{},{mappedValue:s}),this])})).join("")}};const M="@yaireo/tagify/";var I,N={empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},S={wrapper:(t,e)=>`<tags class="${e.classNames.namespace} ${e.mode?`${e.classNames[e.mode+"Mode"]}`:""} ${t.className}"\n ${e.readonly?"readonly":""}\n ${e.disabled?"disabled":""}\n ${e.required?"required":""}\n ${"select"===e.mode?"spellcheck='false'":""}\n tabIndex="-1">\n <span ${!e.readonly&&e.userInput?"contenteditable":""} tabIndex="0" data-placeholder="${e.placeholder||"​"}" aria-placeholder="${e.placeholder||""}"\n class="${e.classNames.input}"\n role="textbox"\n aria-autocomplete="both"\n aria-multiline="${"mix"==e.mode}"></span>\n ​\n </tags>`,tag(t,e){let i=e.settings;return`<tag title="${t.title||t.value}"\n contenteditable='false'\n spellcheck='false'\n tabIndex="${i.a11y.focusableTags?0:-1}"\n class="${i.classNames.tag} ${t.class||""}"\n ${this.getAttributes(t)}>\n <x title='' class="${i.classNames.tagX}" role='button' aria-label='remove tag'></x>\n <div>\n <span class="${i.classNames.tagText}">${t[i.tagTextProp]||t.value}</span>\n </div>\n </tag>`},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return`<div class="${i?"":s} ${e.classname}" role="listbox" aria-labelledby="dropdown">\n <div data-selector='tagify-suggestions-wrapper' class="${t.classNames.dropdownWrapper}"></div>\n </div>`},dropdownContent(t){var e=this.settings,i=this.state.dropdown.suggestions;return`\n ${e.templates.dropdownHeader.call(this,i)}\n ${t}\n ${e.templates.dropdownFooter.call(this,i)}\n `},dropdownItem(t){return`<div ${this.getAttributes(t)}\n class='${this.settings.classNames.dropdownItem} ${t.class?t.class:""}'\n tabindex="0"\n role="option">${t.mappedValue||t.value}</div>`},dropdownHeader(t){return`<header data-selector='tagify-suggestions-header' class="${this.settings.classNames.dropdownHeader}"></header>`},dropdownFooter(t){var e=t.length-this.settings.dropdown.maxItems;return e>0?`<footer data-selector='tagify-suggestions-footer' class="${this.settings.classNames.dropdownFooter}">\n ${e} more items. Refine your search.\n </footer>`:""},dropdownItemNoMatch:null};var E={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)],drop:["input",i.onDrop.bind(this)],compositionstart:["input",i.onCompositionStart.bind(this)],compositionend:["input",i.onCompositionEnd.bind(this)]})this.DOM[e[a][0]][s](a,e[a][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(i.observeOriginalInputValue.bind(this),500);var n=this.listeners.main.inputMutationObserver||new MutationObserver(i.onInputDOMChange.bind(this));n.disconnect(),"mix"==this.settings.mode&&n.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(this.listeners&&(t||!this.listeners.global))for(e of(this.listeners.global=this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)},{type:"click",target:document,cb:i.onClickAnywhere.bind(this)}],this.listeners.global))e.target[s](e.type,e.cb)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=this.settings,i=t.target?this.trim(t.target.textContent):"",s=this.value?.[0]?.[e.tagTextProp],a=t.type,n=e.dropdown.enabled>=0,o={relatedTarget:t.relatedTarget},r=this.state.actions.selectOption&&(n||!e.dropdown.closeOnSelect),l=this.state.actions.addNew&&n,d=t.relatedTarget&&f.call(this,t.relatedTarget)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==a){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),e.onChangeAfterBlur&&this.triggerChangeEvent()}if(!r&&!l)if(this.state.hasFocus="focus"==a&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=e.mode){if("focus"==a)return this.trigger("focus",o),void(0!==e.dropdown.enabled&&e.userInput||this.dropdown.show(this.value.length?"":void 0));"blur"==a&&(this.trigger("blur",o),this.loading(!1),"select"==e.mode&&(d&&(this.removeTags(),i=""),s===i&&(i="")),i&&!this.state.actions.selectOption&&e.addTagOnBlur&&this.addTags(i,!0)),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==a?this.trigger("focus",o):"blur"==t.type&&(this.trigger("blur",o),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onCompositionStart(t){this.state.composing=!0},onCompositionEnd(t){this.state.composing=!1},onWindowKeyDown(t){var e,i=document.activeElement,s=f.call(this,i)&&this.DOM.scope.contains(document.activeElement),a=s&&i.hasAttribute("readonly");if(s&&!a)switch(e=i.nextElementSibling,t.key){case"Backspace":this.settings.readonly||(this.removeTags(i),(e||this.DOM.input).focus());break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;if(!this.state.composing&&e.userInput){"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);if(this.trigger("keydown",{event:t}),"mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var s=document.getSelection(),a="Delete"==t.key&&s.anchorOffset==(s.anchorNode.length||0),n=s.anchorNode.previousSibling,o=1==s.anchorNode.nodeType||!s.anchorOffset&&n&&1==n.nodeType&&s.anchorNode.previousSibling;r(this.DOM.input.innerHTML);var l,h,g,p=this.getTagElms(),c=1===s.anchorNode.length&&s.anchorNode.nodeValue==String.fromCharCode(8203);if("edit"==e.backspace&&o)return l=1==s.anchorNode.nodeType?null:s.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,l),void t.preventDefault();if(m()&&o instanceof Element)return g=d(o),o.hasAttribute("readonly")||o.remove(),this.DOM.input.focus(),void setTimeout((()=>{b(g),this.DOM.input.click()}));if("BR"==s.anchorNode.nodeName)return;if((a||o)&&1==s.anchorNode.nodeType?h=0==s.anchorOffset?a?p[0]:null:p[Math.min(p.length,s.anchorOffset)-1]:a?h=s.anchorNode.nextElementSibling:o instanceof Element&&(h=o),3==s.anchorNode.nodeType&&!s.anchorNode.nodeValue&&s.anchorNode.previousElementSibling&&t.preventDefault(),(o||a)&&!e.backspace)return void t.preventDefault();if("Range"!=s.type&&!s.anchorOffset&&s.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=s.type&&h&&h.hasAttribute("readonly"))return void b(d(h));"Delete"==t.key&&c&&w(s.anchorNode.nextSibling)&&this.removeTags(s.anchorNode.nextSibling),clearTimeout(I),I=setTimeout((()=>{var t=document.getSelection();r(this.DOM.input.innerHTML),!a&&t.anchorNode.previousSibling,this.value=[].map.call(p,((t,e)=>{var i=w(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=t.target.textContent&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible&&"manual"!=e.dropdown.position)return;t.preventDefault(),setTimeout((()=>{this.state.dropdown.visible||this.state.actions.selectOption||this.addTags(i,!0)}))}}},onInput(t){this.postUpdate();var e=this.settings;if("mix"==e.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var i=this.input.normalize.call(this),s=i.length>=e.dropdown.enabled,a={value:i,inputElm:this.DOM.input},n=this.validateTag({value:i});"select"==e.mode&&this.toggleScopeValidation(n),a.isValid=n,this.state.inputText!=i&&(this.input.set.call(this,i,!1),-1!=i.search(e.delimiters)?this.addTags(i)&&this.input.set.call(this):e.dropdown.enabled>=0&&this.dropdown[s?"show":"hide"](i),this.trigger("input",a))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,d=this.settings,h=this.value.length,g=this.getTagElms(),c=document.createDocumentFragment(),u=window.getSelection().getRangeAt(0),v=[].map.call(g,(t=>w(t).value));if("deleteContentBackward"==t.inputType&&m()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),y(this.getTagElms()),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&c.appendChild(this.createTagElem(t))})),c.childNodes.length&&(u.insertNode(c),this.setRangeAtStartEnd(!1,c.lastChild)),g.length!=h)return this.value=[].map.call(this.getTagElms(),(t=>w(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((u=o.getRangeAt(0).cloneRange()).collapse(!0),u.setStart(o.focusNode,0),s=(e=u.toString().slice(0,u.endOffset)).split(d.pattern).length-1,(i=e.match(d.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(d.pattern)[0],value:a.replace(d.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(d.delimiters))return this.state.tag.value=this.state.tag.value.replace(d.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,d.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=d.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s<this.state.mixMode.matchedPatternCount)&&(n=!1)}else this.state.flaggedTags={};this.state.mixMode.matchedPatternCount=s}setTimeout((()=>{this.update({withoutChangeEvent:!0}),this.trigger("input",p({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.parentNode||this.destroy(),this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickAnywhere(t){t.target==this.DOM.scope||this.DOM.scope.contains(t.target)||(this.toggleFocusClass(!1),this.state.hasFocus=!1)},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:w(i),event:t}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide():0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show(this.value.length?"":void 0):"select"!=e.mode||0!==e.dropdown.enabled||this.state.dropdown.visible||this.dropdown.show());this.removeTags(t.target.parentNode)}else this.DOM.input.focus()},onPaste(t){t.preventDefault();var e,i,s=this.settings;if("select"==s.mode&&s.enforceWhitelist||!s.userInput)return!1;s.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),s.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?this.addTags(this.state.inputText+e,!0):this.state.inputText=e)})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=w(i),n=this.input.normalize.call(this,t),o={[this.settings.tagTextProp]:n,__tagId:a.__tagId},r=this.validateTag(o);this.editTagChangeDetected(p(a,o))||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:p({},this.value[s],{newValue:n}),event:e})},onEditTagPaste(t,e){var i=(e.clipboardData||window.clipboardData).getData("Text");e.preventDefault();var s=T(i);this.setRangeAtStartEnd(!1,s)},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i,s=this.settings,a=t.closest("."+s.classNames.tag),n=w(a),o=this.input.normalize.call(this,t),r={[s.tagTextProp]:o,__tagId:n.__tagId},l=n.__originalData,d=this.editTagChangeDetected(p(n,r)),h=this.validateTag(r);if(o)if(d){if(e=this.hasMaxTags(),i=p({},l,{[s.tagTextProp]:this.trim(o),__isValid:h}),s.transformTag.call(this,i,l),!0!==(h=(!e||!0===l.__isValid)&&this.validateTag(i))){if(this.trigger("invalid",{data:i,tag:a,message:h}),s.editTags.keepInvalid)return;s.keepInvalidTags?i.__isValid=h:i=l}else s.keepInvalidTags&&(delete i.title,delete i["aria-invalid"],delete i.class);this.onEditTagDone(a,i)}else this.onEditTagDone(a,l);else this.onEditTagDone(a)}},onEditTagkeydown(t,e){if(!this.state.composing)switch(this.trigger("edit:keydown",{event:t}),t.key){case"Esc":case"Escape":e.parentNode.replaceChild(e.__tagifyTagData.__originalHTML,e),this.state.editing=!1;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=w(s),n=this.settings;s&&n.userInput&&!1!==a.editable&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==n.mode||n.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:w(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if("<div><br></div>"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),b(e)}else if(f.call(this,t))if(3!=t.previousSibling?.nodeType||t.previousSibling.textContent||t.previousSibling.remove(),t.previousSibling&&"BR"==t.previousSibling.nodeName){t.previousSibling.replaceWith("\n"+s);let e=t.nextSibling,i="";for(;e;)i+=e.textContent,e=e.nextSibling;i.trim()&&b(t.previousSibling)}else t.previousSibling&&!w(t.previousSibling)||t.before(s)})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&f.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function _(t,e){if(!t){console.warn("Tagify:","input element not found",t);const e=new Proxy(this,{get:()=>()=>e});return e}if(t.__tagify)return console.warn("Tagify: ","input element is already Tagified - Same instance is returned.",t),t.__tagify;var i;p(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?p({},o):o).tagify=this,s.event&&(o.event=this.cloneEvent(s.event)),s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox=/firefox|fxios/i.test(navigator.userAgent)&&!/seamonkey/i.test(navigator.userAgent),this.isIE=window.document.documentMode,e=e||{},this.getPersistedData=(i=e.id,t=>{let e,s="/"+t;if(1==localStorage.getItem(M+i+"/v",1))try{e=JSON.parse(localStorage[M+i+s])}catch(t){}return e}),this.setPersistedData=(t=>t?(localStorage.setItem(M+t+"/v",1),(e,i)=>{let s="/"+i,a=JSON.stringify(e);e&&i&&(localStorage.setItem(M+t+s,a),dispatchEvent(new Event("storage")))}):()=>{})(e.id),this.clearPersistedData=(t=>e=>{const i=M+"/"+t+"/";if(e)localStorage.removeItem(i+e);else for(let t in localStorage)t.includes(i)&&localStorage.removeItem(t)})(e.id),this.applySettings(t,e),this.state={inputText:"",editing:!1,composing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),O.call(this),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus(),t.__tagify=this}return _.prototype={_dropdown:D,getSetTagData:w,helpers:{sameStr:a,removeCollectionProp:n,omit:o,isObject:g,parseHTML:l,escapeHTML:h,extend:p,concatWithoutDups:c,getUID:v,isNodeTag:f},customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch","dropdown:scroll"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:l,templates:S,parseTemplate(t,e){return l((t=this.settings.templates[t]||t).apply(this,e))},set whitelist(t){const e=t&&Array.isArray(t);this.settings.whitelist=e?t:[],this.setPersistedData(e?t:[],"whitelist")},get whitelist(){return this.settings.whitelist},generateClassSelectors(t){for(let e in t){let i=e;Object.defineProperty(t,i+"Selector",{get(){return"."+this[i].split(" ")[0]}})}},applySettings(t,i){x.templates=this.templates;var s=p({},x,"mix"==i.mode?{dropdown:{position:"text"}}:{}),a=this.settings=p({},s,i);if(a.disabled=t.hasAttribute("disabled"),a.readonly=a.readonly||t.hasAttribute("readonly"),a.placeholder=h(t.getAttribute("placeholder")||a.placeholder||""),a.required=t.hasAttribute("required"),this.generateClassSelectors(a.classNames),void 0===a.dropdown.includeSelectedTags&&(a.dropdown.includeSelectedTags=a.duplicates),this.isIE&&(a.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(a.delimiters))instanceof Array&&(a[e]=i)})),"autoComplete"in i&&!g(i.autoComplete)&&(a.autoComplete=x.autoComplete,a.autoComplete.enabled=i.autoComplete),"mix"==a.mode&&(a.pattern=a.pattern||/@/,a.autoComplete.rightKey=!0,a.delimiters=i.delimiters||null,a.tagTextProp&&!a.dropdown.searchKeys.includes(a.tagTextProp)&&a.dropdown.searchKeys.push(a.tagTextProp)),t.pattern)try{a.pattern=new RegExp(t.pattern)}catch(t){}if(a.delimiters){a._delimiters=a.delimiters;try{a.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}}a.disabled&&(a.userInput=!1),this.TEXTS=e(e({},N),a.texts||{}),("select"!=a.mode||i.dropdown?.enabled)&&a.userInput||(a.dropdown.enabled=0),a.dropdown.appendTarget=i.dropdown?.appendTarget||document.body;let n=this.getPersistedData("whitelist");Array.isArray(n)&&(this.whitelist=Array.isArray(a.whitelist)?c(a.whitelist,n):n)},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!g(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=h(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(t=>{let e=t.value;return"s"==t.unit?1e3*e:e})(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.originalInput_tabIndex=t.tabIndex,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t),t.tabIndex=-1)},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.DOM.originalInput.tabIndex=this.DOM.originalInput_tabIndex,delete this.DOM.originalInput.__tagify,this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout),clearInterval(this.listeners.main.originalInputValueObserverInterval)},loadOriginalValues(t){var e,i=this.settings;if(this.state.blockChangeEvent=!0,void 0===t){const e=this.getPersistedData("value");t=e&&!this.DOM.originalInput.value?e:i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value}if(this.removeAllTags(),t)if("mix"==i.mode)this.parseMixTags(t),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","<br>");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t,!0).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value},cloneEvent(t){var e={};for(var i in t)"path"!=i&&(e[i]=t[i]);return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleScopeValidation(t){var e=!0===t||void 0===t;!this.settings.required&&t&&t===this.TEXTS.empty&&(e=!0),this.toggleClass(this.settings.classNames.tagInvalid,!e),this.DOM.scope.title=e?"":t},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:E,fixFirefoxLastTagNoCaret(){},setRangeAtStartEnd(t,e){if(e){t="number"==typeof t?t:!!t,e=e.lastChild||e;var i=document.getSelection();if(i.focusNode instanceof Element&&!this.DOM.input.contains(i.focusNode))return!0;try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){}}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTagChangeDetected(t){var e=t.__originalData;for(var i in e)if(!this.dataProps.includes(i)&&t[i]!=e[i])return!0;return!1},getTagTextNode(t){return t.querySelector(this.settings.classNames.tagTextSelector)},setTagTextNode(t,e){this.getTagTextNode(t).innerHTML=h(e)},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings,s=this.getTagTextNode(t),a=this.getNodeIndex(t),n=w(t),o=this.events.callbacks,r=this,l=!0;if(s){if(!(n instanceof Object&&"editable"in n)||n.editable)return n=w(t,{__originalData:p({},n),__originalHTML:t.cloneNode(!0)}),w(n.__originalHTML,n.__originalData),s.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),s.addEventListener("focus",o.onEditTagFocus.bind(this,t)),s.addEventListener("blur",(function(){setTimeout((()=>o.onEditTagBlur.call(r,r.getTagTextNode(t))))})),s.addEventListener("input",o.onEditTagInput.bind(this,s)),s.addEventListener("paste",o.onEditTagPaste.bind(this,s)),s.addEventListener("keydown",(e=>o.onEditTagkeydown.call(this,e,t))),s.addEventListener("compositionstart",o.onCompositionStart.bind(this)),s.addEventListener("compositionend",o.onCompositionEnd.bind(this)),e.skipValidation||(l=this.editTagToggleValidity(t)),s.originalIsValid=l,this.trigger("edit:start",{tag:t,index:a,data:n,isValid:l}),s.focus(),this.setRangeAtStartEnd(!1,s),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||w(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid=i,e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:w(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),this.settings.a11y.focusableTags?t.focus():b(t)):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&p(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(w(t))})),this.update()},injectAtCaret(t,e){return!(e=e||this.state.selection?.range)&&t?(this.appendMixTags(t),this):(T(t,e),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this)},input:{set(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=h(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.trim(i)},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{value:""})&&(t={value:t});var e=this.dropdown.getMappedValue(t);if("number"!=typeof e){var i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd(!1,this.DOM.input)),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];var s="."+[...this.settings.classNames.tag.split(" "),...e].join(".");return[].slice.call(this.DOM.scope.querySelectorAll(s))},getLastTag(){var t=this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);return t[t.length-1]},isTagDuplicate(t,e,i){var s=0;if("select"==this.settings.mode)return!1;for(let n of this.value){a(this.trim(""+t),n.value,e)&&i!=n.__tagId&&s++}return s},getTagIndexByValue(t){var e=[],i=this.settings.dropdown.caseSensitive;return this.getTagElms().forEach(((s,n)=>{s.__tagifyTagData&&a(this.trim(s.__tagifyTagData.value),t,i)&&e.push(n)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var s,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(a(o,t,n.dropdown.caseSensitive,n.trim))return s="string"==typeof i?{value:i}:i,!0})),s||"value"!=e||"value"==n.tagTextProp||(s=this.getWhitelistItem(t,n.tagTextProp,i)),s},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?"mix"!=e.mode&&e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,e.dropdown.caseSensitive,t.__tagId)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),this.settings.userInput=!0,this.setContentEditable(!t)},setContentEditable(t){this.settings.userInput&&(this.DOM.input.contentEditable=t,this.DOM.input.tabIndex=t?0:-1)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp,o=[],r=!!i&&i[0]instanceof Object,l=Array.isArray(t),d=l&&t[0].value,h=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=h(t)}else l&&(t=[].concat(...t.map((t=>null!=t.value?t:h(t)))));return r&&!d&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(a.call(this,h),u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize();var d=this.getTagElms();return d.forEach(((t,e)=>w(t,l[e]))),this.update({withoutChangeEvent:!0}),y(d,this.state.hasFocus),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=this.state.selection||window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),-1==(i=n.nodeValue.lastIndexOf(e))?!0:(s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0)}},selectTag(t,e){var i=this.settings;if(!i.enforceWhitelist||this.isTagWhitelisted(e.value)){this.input.set.call(this,e[i.tagTextProp]||e.value,!0),this.state.actions.selectOption&&setTimeout((()=>this.setRangeAtStartEnd(!1,this.DOM.input)));var s=this.getLastTag();return s?this.replaceTag(s,e):this.appendTag(t),this.value[0]=e,this.update(),this.trigger("add",{tag:t,data:e}),[t]}},addEmptyTag(t){var e=p({value:""},t||{}),i=this.createTagElem(e);w(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i){var s=[],a=this.settings,n=[],o=document.createDocumentFragment();if(i=i||a.skipInvalid,!t||0==t.length)return s;switch(t=this.normalizeTags(t),a.mode){case"mix":return this.addMixTags(t);case"select":e=!1,this.removeAllTags()}return this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,r={},l=Object.assign({},t,{value:t.value+""});if(t=Object.assign({},l),a.transformTag.call(this,t),t.__isValid=this.hasMaxTags()||this.validateTag(t),!0!==t.__isValid){if(i)return;if(p(r,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:l}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value)),!a.createInvalidTags)return void n.push(t.value)}if("readonly"in t&&(t.readonly?r["aria-readonly"]=!0:delete t.readonly),e=this.createTagElem(t,r),s.push(e),"select"==a.mode)return this.selectTag(e,t);o.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(o),this.update(),t.length&&e&&(this.input.set.call(this,a.createInvalidTags?"":n.join(a._delimiters)),this.setRangeAtStartEnd(!1,this.DOM.input)),a.dropdown.enabled&&this.dropdown.refilter(),s},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);var e=document.createDocumentFragment();return t.forEach((t=>{var i=this.createTagElem(t);e.appendChild(i)})),this.appendMixTags(e),e},appendMixTags(t){var e=!!this.state.selection;e?this.injectAtCaret(t):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(t),this.updateValueByDOMTags(),this.update())},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;setTimeout(b,0,a)}return this.state.tag=null,this.trigger("add",p({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.input;e.scope.insertBefore(t,i)},createTagElem(t,i){t.__tagId=v();var s,a=p({},t,e({value:h(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a,this])),w(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((e,i)=>{var s=w(e),a=this.hasMaxTags(),n=this.validateTag(s),o=!0===n&&!a;if("select"==t.mode&&this.toggleScopeValidation(n),o)return s=s.__preInvalidData?s.__preInvalidData:{value:s.value},this.replaceTag(e,s);e.title=a||n}))},removeTags(t,e,i){var s,a=this.settings;if(t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>{e&&"string"==typeof e&&(e=this.getTagElmByValue(e));var i=w(e);return e&&i&&!i.readonly&&t.push({node:e,idx:this.getTagIdx(i),data:w(e,{__removed:!0})}),t}),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==a.mode&&(i=0,this.input.set.call(this)),1==s.length&&"select"!=a.mode&&s[0].node.classList.contains(a.classNames.tagNotAllowed)&&(e=!0),s.length)return a.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?a.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),a.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(a.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==a.mode&&this.setContentEditable(!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=w(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.refilter(),this.dropdown.position(),this.state.dropdown.visible&&setTimeout((()=>{this.DOM.input.focus()})),"select"==this.settings.mode&&(this.input.set.call(this),this.setContentEditable(!0)),this.update(t)},postUpdate(){this.state.blockChangeEvent=!1;var t=this.settings,e=t.classNames,i="mix"==t.mode?t.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value.trim():this.value.length+this.input.raw.call(this).length;this.toggleClass(e.hasMaxTags,this.value.length>=t.maxTags),this.toggleClass(e.hasNoTags,!this.value.length),this.toggleClass(e.empty,!i),"select"==t.mode&&this.toggleScopeValidation(this.value?.[0]?.__isValid)},setOriginalInputValue(t){var e=this.DOM.originalInput;this.settings.mixMode.integrated||(e.value=t,e.tagifyValue=e.value,this.setPersistedData(t,"value"))},update(t){clearTimeout(this.debouncedUpdateTimeout),this.debouncedUpdateTimeout=setTimeout(function(){var e=this.getInputValue();this.setOriginalInputValue(e),this.settings.onChangeAfterBlur&&(t||{}).withoutChangeEvent||this.state.blockChangeEvent||this.triggerChangeEvent();this.postUpdate()}.bind(this),100)},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return n(t||this.value,this.dataProps)},getMixedTagsAsString(){var t="",e=this,i=this.settings,s=i.originalInputValueFormat||JSON.stringify,a=i.mixTagsInterpolator;return function i(n){n.childNodes.forEach((n=>{if(1==n.nodeType){const r=w(n);if("BR"==n.tagName&&(t+="\r\n"),r&&f.call(e,n)){if(r.__removed)return;t+=a[0]+s(o(r,e.dataProps))+a[1]}else n.getAttribute("style")||["B","I","U"].includes(n.tagName)?t+=n.textContent:"DIV"!=n.tagName&&"P"!=n.tagName||(t+="\r\n",i(n))}else t+=n.textContent}))}(this.DOM.input),t}},_.prototype.removeTag=_.prototype.removeTags,_}));
\ No newline at end of file diff --git a/cmd/web/templates/articles/htmx-article-page.tmpl b/cmd/web/templates/articles/htmx-article-page.tmpl new file mode 100644 index 0000000..9380c0a --- /dev/null +++ b/cmd/web/templates/articles/htmx-article-page.tmpl @@ -0,0 +1,3 @@ +{{ template "articles/show" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/articles/htmx-post-comments.tmpl b/cmd/web/templates/articles/htmx-post-comments.tmpl new file mode 100644 index 0000000..742b339 --- /dev/null +++ b/cmd/web/templates/articles/htmx-post-comments.tmpl @@ -0,0 +1,2 @@ +{{ template "articles/partials/comments-card" . }} +{{ template "articles/partials/comments-form" . }}
\ No newline at end of file diff --git a/cmd/web/templates/articles/htmx-show.tmpl b/cmd/web/templates/articles/htmx-show.tmpl new file mode 100644 index 0000000..9021bb2 --- /dev/null +++ b/cmd/web/templates/articles/htmx-show.tmpl @@ -0,0 +1,3 @@ +{{ template "articles/partials/detail-title" . }} +{{ template "articles/partials/detail-post-meta" . }} +{{ template "articles/partials/detail-post-content" . }}
\ No newline at end of file diff --git a/cmd/web/templates/articles/partials/comments-card.tmpl b/cmd/web/templates/articles/partials/comments-card.tmpl new file mode 100644 index 0000000..1e6062a --- /dev/null +++ b/cmd/web/templates/articles/partials/comments-card.tmpl @@ -0,0 +1,25 @@ +<div class="card"> + <div class="card-block"> + <p class="card-text">{{ .Comment.Body }}</p> + </div> + <div class="card-footer"> + <a href="/users/{{ .Comment.User.Username }}" + hx-push-url="/users/{{ .Comment.User.Username }}" + hx-get="/htmx/users/{{ .Comment.User.Username }}" + hx-target="#app-body" + class="comment-author" + > + <img src="{{ .Comment.User.Image }}" class="comment-author-img" /> + </a> + + <a href="/users/{{ .Comment.User.Username }}" + hx-push-url="/users/{{ .Comment.User.Username }}" + hx-get="/htmx/users/{{ .Comment.User.Username }}" + hx-target="#app-body" + class="comment-author" + > + {{ .Comment.User.Name }} + </a> + <span class="date-posted">{{ .Comment.GetFormattedCreatedAt }}</span> + </div> +</div>
\ No newline at end of file diff --git a/cmd/web/templates/articles/partials/comments-form.tmpl b/cmd/web/templates/articles/partials/comments-form.tmpl new file mode 100644 index 0000000..09f9697 --- /dev/null +++ b/cmd/web/templates/articles/partials/comments-form.tmpl @@ -0,0 +1,18 @@ +<form id="article-comment-form" class="card comment-form" + hx-post="/htmx/articles/{{ .Article.Slug }}/comments" + hx-target="#article-comments-wrapper" hx-swap="afterbegin show:top" + + {{ if .IsOob }} + hx-swap-oob="true" + {{ end }} +> + <div class="card-block"> + <textarea class="form-control" placeholder="Write a comment..." rows="3" name="comment"></textarea> + </div> + <div class="card-footer"> + <img src="{{ .Article.User.Image }}" class="comment-author-img" /> + <button class="btn btn-sm btn-primary"> + Post Comment + </button> + </div> +</form>
\ No newline at end of file diff --git a/cmd/web/templates/articles/partials/comments-wrapper.tmpl b/cmd/web/templates/articles/partials/comments-wrapper.tmpl new file mode 100644 index 0000000..266b4e5 --- /dev/null +++ b/cmd/web/templates/articles/partials/comments-wrapper.tmpl @@ -0,0 +1,26 @@ +<div id="article-comments-wrapper"> + {{ range $comment := .Article.Comments }} + {{ template "articles/partials/comments-card" Dict "Comment" $comment }} + {{ end }} +</div> + +{{ if .IsAuthenticated }} + <div id="form-message"></div> + + {{ template "articles/partials/comments-form" . }} +{{ else }} + <div> + <a href="/htmx/sign-in" hx-get="/htmx/sign-in" hx-target="#app-body" + hx-push-url="/sign-in" + > + Sign in + </a> + or + <a href="/htmx/sign-up" hx-get="/htmx/sign-up" hx-target="#app-body" + hx-push-url="/sign-up" + > + sign up + </a> + to add comments on this article. + </div> +{{ end }}
\ No newline at end of file diff --git a/cmd/web/templates/articles/partials/detail-post-meta.tmpl b/cmd/web/templates/articles/partials/detail-post-meta.tmpl new file mode 100644 index 0000000..a871b44 --- /dev/null +++ b/cmd/web/templates/articles/partials/detail-post-meta.tmpl @@ -0,0 +1,42 @@ +<div class="post-meta"> + <a href="#"><img src="{{ .Article.User.Image }}" /></a> + <div class="info"> + <a href="/users/{{ .Article.User.Username }}" + hx-push-url="/users/{{ .Article.User.Username }}" + hx-get="/htmx/users/{{ .Article.User.Username }}" + hx-target="#app-body" + class="author" + > + {{ .Article.User.Name }} + </a> + <span class="date">{{ .Article.GetFormattedCreatedAt }}</span> + </div> + + {{ if .IsSelf }} + + <button class="btn btn-outline-secondary btn-sm edit-button" + hx-get="/htmx/editor/{{ .Article.Slug }}" + hx-target="#app-body" + hx-push-url="/editor/{{ .Article.Slug }}" + > + <i class="ion-edit"></i> + Edit Article + </button> + + <button class="btn btn-outline-danger btn-sm delete-button" + hx-delete="/htmx/articles/{{ .Article.Slug }}" + hx-target="#app-body" + hx-confirm="Are you sure you wish to delete the article?" + > + <i class="ion-trash-a"></i> + Delete Article + </button> + + {{ else }} + + {{ template "articles/partials/follow-button" . }} + + {{ template "articles/partials/favorite-button" . }} + + {{ end }} +</div>
\ No newline at end of file diff --git a/cmd/web/templates/articles/partials/favorite-button.tmpl b/cmd/web/templates/articles/partials/favorite-button.tmpl new file mode 100644 index 0000000..3146830 --- /dev/null +++ b/cmd/web/templates/articles/partials/favorite-button.tmpl @@ -0,0 +1,15 @@ +<button class="btn btn-outline-primary btn-sm {{ if .IsArticleFavorited }} active {{ end }} favorite-button" + hx-post="/htmx/articles/{{ .Article.Slug }}/favorite" + + {{ if .IsOob }} + hx-swap-oob="outerHTML:.favorite-button" + {{ end }} +> + <i class="ion-heart"></i> + {{ if .IsArticleFavorited }} + Unfavorite Post + {{ else }} + Favorite Post + {{ end }} + ({{ .Article.GetFavoriteCount }}) +</button>
\ No newline at end of file diff --git a/cmd/web/templates/articles/partials/follow-button.tmpl b/cmd/web/templates/articles/partials/follow-button.tmpl new file mode 100644 index 0000000..7184278 --- /dev/null +++ b/cmd/web/templates/articles/partials/follow-button.tmpl @@ -0,0 +1,17 @@ +<button class="btn btn-sm btn-outline-secondary follow-button" + hx-post="/htmx/articles/follow-user/{{ .Article.Slug }}" + + {{ if .IsOob }} + hx-swap-oob="outerHTML:.follow-button" + {{ end }} +> + {{ if .IsFollowed }} + <i class="ion-minus-round"></i> + Unfollow + {{ else }} + <i class="ion-plus-round"></i> + Follow + {{ end }} + {{ .Article.User.Name }} + <span class="counter">({{ .Article.User.FollowersCount }})</span> +</button>
\ No newline at end of file diff --git a/cmd/web/templates/articles/show.tmpl b/cmd/web/templates/articles/show.tmpl new file mode 100644 index 0000000..364444e --- /dev/null +++ b/cmd/web/templates/articles/show.tmpl @@ -0,0 +1,38 @@ +<div class="post-page"> + + <div class="banner"> + <div class="container"> + <h1 id="article-detail__title"> + {{ .Article.Title }} + </h1> + + {{ template "articles/partials/detail-post-meta" . }} + </div> + </div> + + <div class="article-detail container page"> + <div class="row post-content"> + <div class="col-md-12"> + {{ .Article.Body }} + </div> + <div class="col-md-12 m-t-2"> + <ul class="tag-list"> + {{ range $tag := .Article.Tags }} + <li class="tag-default tag-pill tag-outline">{{ $tag.Name }}</li> + {{ end }} + </ul> + </div> + </div> + + <hr /> + + <div class="post-actions"> + {{ template "articles/partials/detail-post-meta" . }} + </div> + + <div class="row"> + <div class="col-md-8 col-md-offset-2" hx-get="/htmx/articles/{{ .Article.Slug }}/comments" hx-trigger="load"></div> + </div> + + </div> +</div>
\ No newline at end of file diff --git a/cmd/web/templates/components/error-message.tmpl b/cmd/web/templates/components/error-message.tmpl new file mode 100644 index 0000000..e4a913e --- /dev/null +++ b/cmd/web/templates/components/error-message.tmpl @@ -0,0 +1,15 @@ +<div id="form-message" + {{ if .IsOob }} + hx-swap-oob="true" + {{ end }} +> + {{ if .Errors }} + <div class="alert alert-danger"> + <ul> + {{ range $error := .Errors }} + <li>{{ $error }}</li> + {{ end }} + </ul> + </div> + {{ end }} +</div>
\ No newline at end of file diff --git a/cmd/web/templates/components/head.tmpl b/cmd/web/templates/components/head.tmpl new file mode 100644 index 0000000..235ae63 --- /dev/null +++ b/cmd/web/templates/components/head.tmpl @@ -0,0 +1,5 @@ +<head> + {{ if ne .PageTitle "" }} + <title>{{ .PageTitle }} — Projecty</title> + {{ end }} +</head> diff --git a/cmd/web/templates/components/navbar.tmpl b/cmd/web/templates/components/navbar.tmpl new file mode 100644 index 0000000..f9cdc2a --- /dev/null +++ b/cmd/web/templates/components/navbar.tmpl @@ -0,0 +1,90 @@ +<ul id="navbar" class="nav navbar-nav pull-xs-right" + hx-swap-oob="true" +> + <li class="nav-item"> + <a id="nav-link-home" + {{ if ne .NavBarActive "home" }} + href="/" + hx-get="/htmx/home" + hx-target="#app-body" + hx-push-url="/" + {{ end }} + class="nav-link{{ if eq .NavBarActive "home" }} active {{ end }}" + > + Home + </a> + </li> + + {{ if not (IsAuthenticated .FiberCtx) }} + <li class="nav-item"> + <a id="nav-link-sign-in" + {{ if ne .NavBarActive "sign-in" }} + href="/sign-in" + hx-get="/htmx/sign-in" + hx-target="#app-body" + hx-push-url="/sign-in" + {{ end }} + class="nav-link {{ if eq .NavBarActive "sign-in" }} active {{ end }}" + > + Sign in + </a> + </li> + <li class="nav-item"> + <a id="nav-link-sign-up" + {{ if ne .NavBarActive "sign-up" }} + href="/sign-up" + hx-get="/htmx/sign-up" + hx-target="#app-body" + hx-push-url="/sign-up" + {{ end }} + class="nav-link {{ if eq .NavBarActive "sign-up" }} active {{ end }}" + > + Sign up + </a> + </li> + {{ end }} + + {{ if IsAuthenticated .FiberCtx }} + <li class="nav-item"> + <a id="nav-link-editor" + {{ if ne .NavBarActive "editor" }} + href="/editor" + hx-get="/htmx/editor" + hx-target="#app-body" + hx-push-url="/editor" + {{ end }} + class="nav-link {{ if eq .NavBarActive "editor" }} active {{ end }}" + > + <i class="ion-compose"></i> + New Article + </a> + </li> + <li class="nav-item"> + <a id="nav-link-settings" + {{ if ne .NavBarActive "settings" }} + href="/settings" + hx-get="/htmx/settings" + hx-target="#app-body" + hx-push-url="/settings" + {{ end }} + class="nav-link {{ if eq .NavBarActive "settings" }} active {{ end }}" + > + Settings + </a> + </li> + <li class="nav-item"> + <a id="nav-link-profile" + {{ if ne .NavBarActive "profile" }} + href="/users/{{ .AuthenticatedUser.Username }}" + hx-get="/htmx/users/{{ .AuthenticatedUser.Username }}" + hx-target="#app-body" + hx-push-url="/users/{{ .AuthenticatedUser.Username }}" + {{ end }} + class="nav-link {{ if eq .NavBarActive "profile" }} active {{ end }}" + > + <img class="user-pic" src="{{ .AuthenticatedUser.Image }}"> + {{ .AuthenticatedUser.Name }} + </a> + </li> + {{ end }} +</ul>
\ No newline at end of file diff --git a/cmd/web/templates/components/redirect.tmpl b/cmd/web/templates/components/redirect.tmpl new file mode 100644 index 0000000..1a706db --- /dev/null +++ b/cmd/web/templates/components/redirect.tmpl @@ -0,0 +1,6 @@ +<div id="htmx-redirect" + hx-target="{{ .HXTarget }}" + hx-trigger="{{ .HXTrigger }}" + hx-get="{{ .HXGet }}" + hx-swap-oob="true" +></div>
\ No newline at end of file diff --git a/cmd/web/templates/editor/form-message.tmpl b/cmd/web/templates/editor/form-message.tmpl new file mode 100644 index 0000000..1707e78 --- /dev/null +++ b/cmd/web/templates/editor/form-message.tmpl @@ -0,0 +1,19 @@ + {{ if .Errors }} + <div class="alert alert-danger"> + <ul> + {{ range $error := .Errors }} + <li>{{ $error }}</li> + {{ end }} + </ul> + </div> + {{ end }} + + {{ if .SuccessMessages }} + <div class="alert alert-success"> + <ul> + {{ range $message := .SuccessMessages }} + <li>{{ $message }}</li> + {{ end }} + </ul> + </div> + {{ end }}
\ No newline at end of file diff --git a/cmd/web/templates/editor/form.tmpl b/cmd/web/templates/editor/form.tmpl new file mode 100644 index 0000000..e735f75 --- /dev/null +++ b/cmd/web/templates/editor/form.tmpl @@ -0,0 +1,48 @@ +<div class="editor-page"> + <div class="container page"> + <div class="row"> + + <div class="col-md-10 col-md-offset-1 col-xs-12"> + + <div id="form-message"> + {{ template "editor/form-message" . }} + </div> + + <form method="post" + + {{ if .HasArticle }} + hx-patch="/htmx/editor/{{ .Article.Slug }}" + {{ else }} + hx-post="/htmx/editor" + {{ end }} + + hx-target="#app-body" + > + <fieldset class="form-group"> + <input type="text" name="title" class="form-control form-control-lg" placeholder="Post Title" + value="{{ .Article.Title }}" + > + </fieldset> + <fieldset class="form-group"> + <input type="text" name="description" class="form-control form-control-md" placeholder="What's this article about?" + value="{{ .Article.Description }}" + > + </fieldset> + <fieldset class="form-group"> + <textarea rows="8" name="content" class="form-control" placeholder="Write your post (in markdown)">{{ .Article.Body }}</textarea> + </fieldset> + <fieldset class="form-group"> + <input type="text" name="tags" class="form-control tagify--outside" placeholder="Enter tags" + {{ if .HasArticle }} + value="{{ .Article.GetTagsAsCommaSeparated }}" + {{ end }} + > + </fieldset> + <button class="btn btn-lg btn-primary pull-xs-right"> + Publish Article + </button> + </form> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/cmd/web/templates/editor/htmx-editor-page.tmpl b/cmd/web/templates/editor/htmx-editor-page.tmpl new file mode 100644 index 0000000..ef5d8cd --- /dev/null +++ b/cmd/web/templates/editor/htmx-editor-page.tmpl @@ -0,0 +1,3 @@ +{{ template "editor/form" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/home/htmx-home-feed.tmpl b/cmd/web/templates/home/htmx-home-feed.tmpl new file mode 100644 index 0000000..eb24863 --- /dev/null +++ b/cmd/web/templates/home/htmx-home-feed.tmpl @@ -0,0 +1,3 @@ +{{ template "home/partials/post-preview" . }} +{{ template "home/partials/feed-navigation" . }} +{{ template "home/partials/pagination" . }}
\ No newline at end of file diff --git a/cmd/web/templates/home/htmx-home-page.tmpl b/cmd/web/templates/home/htmx-home-page.tmpl new file mode 100644 index 0000000..575f328 --- /dev/null +++ b/cmd/web/templates/home/htmx-home-page.tmpl @@ -0,0 +1,3 @@ +{{ template "home/index" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/home/index.tmpl b/cmd/web/templates/home/index.tmpl new file mode 100644 index 0000000..65fee1d --- /dev/null +++ b/cmd/web/templates/home/index.tmpl @@ -0,0 +1,45 @@ +<div class="home-page"> + <div class="banner"> + <div class="container"> + <h1 class="logo-font">Projecty</h1> + <p>A place to share your projects.</p> + </div> + </div> + + <div class="container page"> + <div class="row"> + + <div class="col-md-9"> + <div class="feed-toggle"> + <ul id="feed-navigation" class="nav nav-pills outline-active"></ul> + </div> + + <div id="feed-post-preview" + hx-trigger="load" + + {{ if .Tag }} + hx-get="/htmx/home/tag-feed/{{ .TagSlug }}?page={{ .CurrentPage }}" + {{ else if .Personal }} + hx-get="/htmx/home/your-feed?page={{ .CurrentPage }}" + {{ else }} + hx-get="/htmx/home/global-feed?page={{ .CurrentPage }}" + {{ end }} + ></div> + + <nav id="feed-pagination"></nav> + </div> + + <div class="col-md-3"> + <div class="sidebar"> + <p>Popular Tags</p> + + <div id="popular-tag-list" class="tag-list" + hx-trigger="load" + hx-get="/htmx/home/tag-list" + ></div> + </div> + </div> + + </div> + </div> +</div> diff --git a/cmd/web/templates/home/partials/article-favorite-button.tmpl b/cmd/web/templates/home/partials/article-favorite-button.tmpl new file mode 100644 index 0000000..c26226a --- /dev/null +++ b/cmd/web/templates/home/partials/article-favorite-button.tmpl @@ -0,0 +1,6 @@ +<button class="btn btn-outline-primary btn-sm pull-xs-right {{ if .IsFavorited }} active {{ end }}" + hx-post="/htmx/home/articles/{{ .Slug }}/favorite" + hx-swap="outerHTML" +> + <i class="ion-heart"></i> {{ .GetFavoriteCount }} +</button>
\ No newline at end of file diff --git a/cmd/web/templates/home/partials/feed-navigation.tmpl b/cmd/web/templates/home/partials/feed-navigation.tmpl new file mode 100644 index 0000000..0a2356a --- /dev/null +++ b/cmd/web/templates/home/partials/feed-navigation.tmpl @@ -0,0 +1,17 @@ +<ul id="feed-navigation" class="nav nav-pills outline-active" hx-swap-oob="true"> + {{ range $item := .FeedNavbarItems }} + <li class="nav-item"> + <a class="nav-link {{ if $item.IsActive }} active {{ end }}" + {{ if not $item.IsActive }} + href="{{ $item.HXPushURL }}" + hx-get="{{ $item.HXGetURL }}" + hx-trigger="click" + hx-target="#feed-post-preview" + hx-push-url="{{ $item.HXPushURL }}" + {{ end }} + > + {{ $item.Title }} + </a> + </li> + {{ end }} +</ul>
\ No newline at end of file diff --git a/cmd/web/templates/home/partials/pagination.tmpl b/cmd/web/templates/home/partials/pagination.tmpl new file mode 100644 index 0000000..2ca4042 --- /dev/null +++ b/cmd/web/templates/home/partials/pagination.tmpl @@ -0,0 +1,18 @@ +<nav id="feed-pagination" hx-swap-oob="true"> + {{ if .HasPagination }} + {{ $CurrentPagination := .CurrentPagination }} + {{ $PathPagination := .PathPagination }} + {{ $PushPathPagination := .PushPathPagination }} + <ul class="pagination"> + {{ range $index := Iterate 1 .TotalPagination }} + <li class="page-item {{ if eq $CurrentPagination $index }} active {{ end }}"> + <a class="page-link" + href="/{{ $PathPagination }}?page={{ $index }}" + hx-push-url="/{{ $PushPathPagination }}?page={{ $index }}" + hx-get="/htmx/home/{{ $PathPagination }}?page={{ $index }}" + >{{ $index }}</a> + </li> + {{ end }} + </ul> + {{ end }} +</nav> diff --git a/cmd/web/templates/home/partials/post-preview.tmpl b/cmd/web/templates/home/partials/post-preview.tmpl new file mode 100644 index 0000000..7ef2111 --- /dev/null +++ b/cmd/web/templates/home/partials/post-preview.tmpl @@ -0,0 +1,60 @@ +<div id="feed-post-preview" hx-swap-oob="true"> + {{ if .HasArticles }} + {{ range $article := .Articles }} + + <div class="post-preview"> + <div class="post-meta"> + <a href="/users/{{ $article.User.Username }}" + hx-push-url="/users/{{ $article.User.Username }}" + hx-get="/htmx/users/{{ $article.User.Username }}" + hx-target="#app-body" + > + <img src="{{ $article.User.Image }}" /> + </a> + + <div class="info"> + <a href="/users/{{ $article.User.Username }}" + hx-push-url="/users/{{ $article.User.Username }}" + hx-get="/htmx/users/{{ $article.User.Username }}" + hx-target="#app-body" + class="author" + > + {{ $article.User.Name }} + </a> + <span class="date">{{ $article.GetFormattedCreatedAt }}</span> + </div> + + {{ template "home/partials/article-favorite-button" $article }} + + </div> + <a href="/articles/{{ $article.Slug }}" + hx-push-url="/articles/{{ $article.Slug }}" + hx-get="/htmx/articles/{{ $article.Slug }}" + hx-target="#app-body" + class="preview-link" + > + <h1>{{ $article.Title }}</h1> + <p>{{ $article.Description }}</p> + + <div class="m-t-1"> + <span>Read more...</span> + + <ul class="tag-list"> + {{ range $tag := $article.Tags }} + <li class="tag-default tag-pill tag-outline">{{ $tag.Name }}</li> + {{ end }} + </ul> + </div> + </a> + </div> + {{ end }} + {{ end }} + + {{ if not .HasArticles }} + <div class="post-preview"> + <div class="alert alert-warning" role="alert"> + No articles are here... yet. + </div> + </div> + {{ end }} +</div>
\ No newline at end of file diff --git a/cmd/web/templates/home/partials/tag-item-list.tmpl b/cmd/web/templates/home/partials/tag-item-list.tmpl new file mode 100644 index 0000000..eac4e53 --- /dev/null +++ b/cmd/web/templates/home/partials/tag-item-list.tmpl @@ -0,0 +1,12 @@ +<div id="popular-tag-list" class="tag-list" hx-swap-oob="true"> + {{ if .HasTags }} + {{ range $tag := .Tags }} + <a class="label label-pill label-default" + href="/tag-feed/{{ $tag.Name }}" + hx-get="/htmx/home/tag-feed/{{ $tag.Name }}" + hx-target="#feed-post-preview" + hx-push-url="/tag-feed/{{ $tag.Name }}" + >{{ $tag.Name }}</a> + {{ end }} + {{ end }} +</div>
\ No newline at end of file diff --git a/cmd/web/templates/layouts/app-htmx.tmpl b/cmd/web/templates/layouts/app-htmx.tmpl new file mode 100644 index 0000000..fa6e6dc --- /dev/null +++ b/cmd/web/templates/layouts/app-htmx.tmpl @@ -0,0 +1 @@ +{{ embed }}
\ No newline at end of file diff --git a/cmd/web/templates/layouts/app.tmpl b/cmd/web/templates/layouts/app.tmpl new file mode 100644 index 0000000..986f458 --- /dev/null +++ b/cmd/web/templates/layouts/app.tmpl @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>{{ .PageTitle }}</title> + <link rel="stylesheet" href="/static/css/style.css"> + <link rel="stylesheet" href="/static/css/tagify.css"> + + <style> + .tagify--outside{ + border: 0; + } + + .tagify--outside .tagify__input{ + order: -1; + flex: 100%; + border: 1px solid var(--tags-border-color); + margin-bottom: 1em; + transition: .1s; + } + + .tagify--outside .tagify__input:hover{ border-color:var(--tags-hover-border-color); } + .tagify--outside.tagify--focus .tagify__input{ + transition:0s; + border-color: var(--tags-focus-border-color); + } + + .tagify__input { border-radius: 4px; margin: 0; padding: 10px 12px; } + </style> + + <link href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"> + <link href="https://fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css"> + </head> + <body hx-ext="head-support"> + <nav class="navbar navbar-light"> + <div class="container"> + <a class="navbar-brand" + href="/" + hx-push-url="/" + hx-get="/htmx/home" + hx-target="#app-body">Projecty</a> + + {{ template "components/navbar" . }} + </div> + </nav> + + <div id="app-body"> + {{ embed }} + </div> + + <footer> + <div class="container"> + <a href="/" class="logo-font">Projecty</a> + <span class="attribution"> + An interactive personal project development and sharing website. + </span> + </div> + </footer> + + <div id="htmx-redirect"></div> + + <script src="/static/js/tagify.js"></script> + <script src="/static/js/htmx.js"></script> + <script src="/static/js/htmx-head-support.js"></script> + + <script> + var isTagify = null; + + window.addEventListener('DOMContentLoaded', function() { + renderTagify(); + }); + + document.body.addEventListener("htmx:afterSwap", function(evt) { + renderTagify(); + }); + + function renderTagify() { + const input = document.querySelector('input[name=tags]'); + const tagify = document.querySelector('tags[class="tagify form-control tagify--outside"]'); + + if (input && !tagify) { + new Tagify(input, { + whitelist: [], + dropdown: { + position: "input", + enabled : 0 // always opens dropdown when input gets focus + } + }) + } + } + </script> + </body> +</html> diff --git a/cmd/web/templates/settings/htmx-setting-page.tmpl b/cmd/web/templates/settings/htmx-setting-page.tmpl new file mode 100644 index 0000000..618c17f --- /dev/null +++ b/cmd/web/templates/settings/htmx-setting-page.tmpl @@ -0,0 +1,3 @@ +{{ template "settings/index" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/settings/index.tmpl b/cmd/web/templates/settings/index.tmpl new file mode 100644 index 0000000..321b169 --- /dev/null +++ b/cmd/web/templates/settings/index.tmpl @@ -0,0 +1,22 @@ +<div class="settings-page"> + <div class="container page"> + <div class="row"> + + <div class="col-md-6 col-md-offset-3 col-xs-12"> + <h1 class="text-xs-center">Your Settings</h1> + + {{ template "settings/partials/form-message" }} + + {{ template "settings/partials/form" . }} + </div> + + <div class="col-md-6 col-md-offset-3"> + <hr> + <button class="btn btn-outline-danger" hx-post="/htmx/sign-out"> + Or click here to logout. + </button> + </div> + + </div> + </div> +</div>
\ No newline at end of file diff --git a/cmd/web/templates/settings/partials/form-message.tmpl b/cmd/web/templates/settings/partials/form-message.tmpl new file mode 100644 index 0000000..44b855f --- /dev/null +++ b/cmd/web/templates/settings/partials/form-message.tmpl @@ -0,0 +1,25 @@ +<div id="settings-form-messages" + {{ if .IsOob }} + hx-swap-oob="true" + {{ end }} +> + {{ if .Errors }} + <div class="alert alert-danger"> + <ul> + {{ range $error := .Errors }} + <li>{{ $error }}</li> + {{ end }} + </ul> + </div> + {{ end }} + + {{ if .SuccessMessages }} + <div class="alert alert-success"> + <ul> + {{ range $message := .SuccessMessages }} + <li>{{ $message }}</li> + {{ end }} + </ul> + </div> + {{ end }} +</div>
\ No newline at end of file diff --git a/cmd/web/templates/settings/partials/form.tmpl b/cmd/web/templates/settings/partials/form.tmpl new file mode 100644 index 0000000..c0d8c4a --- /dev/null +++ b/cmd/web/templates/settings/partials/form.tmpl @@ -0,0 +1,25 @@ +<form + action="/settings" + method="POST" + hx-post="/htmx/settings" + id="settings-form" +> + <fieldset class="form-group"> + <input class="form-control" type="text" placeholder="URL of profile picture" value="{{ .AuthenticatedUser.Image }}" name="image_url"> + </fieldset> + <fieldset class="form-group"> + <input class="form-control form-control-lg" type="text" placeholder="Your Name" value="{{ .AuthenticatedUser.Name }}" name="name"> + </fieldset> + <fieldset class="form-group"> + <textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you" name="bio">{{ .AuthenticatedUser.Bio }}</textarea> + </fieldset> + <fieldset class="form-group"> + <input class="form-control form-control-lg" type="email" placeholder="Email" value="{{ .AuthenticatedUser.Email }}" name="email"> + </fieldset> + <fieldset class="form-group"> + <input class="form-control form-control-lg" type="password" placeholder="Password" name="password"> + </fieldset> + <button class="btn btn-lg btn-primary pull-xs-right" hx-post="/htmx/settings" hx-swap="none"> + Update Settings + </button> +</form>
\ No newline at end of file diff --git a/cmd/web/templates/settings/partials/htmx-form-message.tmpl b/cmd/web/templates/settings/partials/htmx-form-message.tmpl new file mode 100644 index 0000000..1e5f5bc --- /dev/null +++ b/cmd/web/templates/settings/partials/htmx-form-message.tmpl @@ -0,0 +1,2 @@ +{{ template "settings/partials/form-message" . }} +{{ template "components/navbar" . }}
\ No newline at end of file diff --git a/cmd/web/templates/sign-in/htmx-sign-in-page.tmpl b/cmd/web/templates/sign-in/htmx-sign-in-page.tmpl new file mode 100644 index 0000000..3201ee8 --- /dev/null +++ b/cmd/web/templates/sign-in/htmx-sign-in-page.tmpl @@ -0,0 +1,3 @@ +{{ template "sign-in/index" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/sign-in/index.tmpl b/cmd/web/templates/sign-in/index.tmpl new file mode 100644 index 0000000..8f6806d --- /dev/null +++ b/cmd/web/templates/sign-in/index.tmpl @@ -0,0 +1,23 @@ +<div class="auth-page"> + <div class="container page"> + <div class="row"> + + <div class="col-md-6 col-md-offset-3 col-xs-12"> + <h1 class="text-xs-center">Sign in</h1> + <p class="text-xs-center"> + <a + href="/sign-up" + hx-push-url="/sign-up" + hx-get="/htmx/sign-up" + hx-target="#app-body" + > + Need an account? + </a> + </p> + + {{ template "sign-in/partials/sign-in-form" }} + </div> + + </div> + </div> +</div>
\ No newline at end of file diff --git a/cmd/web/templates/sign-in/partials/sign-in-form.tmpl b/cmd/web/templates/sign-in/partials/sign-in-form.tmpl new file mode 100644 index 0000000..21f2c84 --- /dev/null +++ b/cmd/web/templates/sign-in/partials/sign-in-form.tmpl @@ -0,0 +1,27 @@ +<div id="sign-in-form-messages" + {{ if .IsOob }} + hx-swap-oob="true" + {{ end }} +> + {{ if .Errors }} + <div class="alert alert-danger"> + <ul> + {{ range $error := .Errors }} + <li>{{ $error }}</li> + {{ end }} + </ul> + </div> + {{ end }} +</div> + +<form method="POST" hx-post="/htmx/sign-in"> + <fieldset class="form-group"> + <input type="text" id="sign-in-email" class="form-control form-control-lg" name="email" placeholder="Email"> + </fieldset> + <fieldset class="form-group"> + <input type="password" id="sign-in-password" class="form-control form-control-lg" name="password" placeholder="Password"> + </fieldset> + <button class="btn btn-lg btn-primary pull-xs-right"> + Sign in + </button> +</form>
\ No newline at end of file diff --git a/cmd/web/templates/sign-up/htmx-sign-up-page.tmpl b/cmd/web/templates/sign-up/htmx-sign-up-page.tmpl new file mode 100644 index 0000000..a4cf3d7 --- /dev/null +++ b/cmd/web/templates/sign-up/htmx-sign-up-page.tmpl @@ -0,0 +1,3 @@ +{{ template "sign-up/index" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/sign-up/index.tmpl b/cmd/web/templates/sign-up/index.tmpl new file mode 100644 index 0000000..2c0992e --- /dev/null +++ b/cmd/web/templates/sign-up/index.tmpl @@ -0,0 +1,23 @@ +<div class="auth-page"> + <div class="container page"> + <div class="row"> + + <div class="col-md-6 col-md-offset-3 col-xs-12"> + <h1 class="text-xs-center">Sign up</h1> + <p class="text-xs-center"> + <a + href="/sign-in" + hx-push-url="/sign-in" + hx-get="/htmx/sign-in" + hx-target="#app-body" + > + Have an account? + </a> + </p> + + {{ template "sign-up/partials/sign-up-form" }} + </div> + + </div> + </div> +</div>
\ No newline at end of file diff --git a/cmd/web/templates/sign-up/partials/sign-up-form.tmpl b/cmd/web/templates/sign-up/partials/sign-up-form.tmpl new file mode 100644 index 0000000..be34243 --- /dev/null +++ b/cmd/web/templates/sign-up/partials/sign-up-form.tmpl @@ -0,0 +1,30 @@ +<div id="sign-up-form-messages" + {{ if .IsOob }} + hx-swap-oob="true" + {{ end }} +> + {{ if .Errors }} + <div class="alert alert-danger"> + <ul> + {{ range $error := .Errors }} + <li>{{ $error }}</li> + {{ end }} + </ul> + </div> + {{ end }} +</div> + +<form method="POST" hx-post="/htmx/sign-up"> +<fieldset class="form-group"> + <input id="sign-up-username" class="form-control form-control-lg" type="text" name="username" placeholder="Username"> +</fieldset> +<fieldset class="form-group"> + <input id="sign-up-email" class="form-control form-control-lg" type="text" name="email" placeholder="Email"> +</fieldset> +<fieldset class="form-group"> + <input id="sign-up-password" class="form-control form-control-lg" type="password" name="password" placeholder="Password"> +</fieldset> +<button class="btn btn-lg btn-primary pull-xs-right"> + Sign up +</button> +</form>
\ No newline at end of file diff --git a/cmd/web/templates/users/htmx-users-articles.tmpl b/cmd/web/templates/users/htmx-users-articles.tmpl new file mode 100644 index 0000000..d002097 --- /dev/null +++ b/cmd/web/templates/users/htmx-users-articles.tmpl @@ -0,0 +1,2 @@ +{{ template "users/partials/post-preview" . }} +{{ template "users/partials/feed-navigation" . }}
\ No newline at end of file diff --git a/cmd/web/templates/users/htmx-users-page.tmpl b/cmd/web/templates/users/htmx-users-page.tmpl new file mode 100644 index 0000000..020649e --- /dev/null +++ b/cmd/web/templates/users/htmx-users-page.tmpl @@ -0,0 +1,3 @@ +{{ template "users/show" . }} +{{ template "components/navbar" . }} +{{ template "components/head" . }}
\ No newline at end of file diff --git a/cmd/web/templates/users/partials/article-favorite-button.tmpl b/cmd/web/templates/users/partials/article-favorite-button.tmpl new file mode 100644 index 0000000..c63182a --- /dev/null +++ b/cmd/web/templates/users/partials/article-favorite-button.tmpl @@ -0,0 +1,12 @@ +<button class="btn btn-outline-primary btn-sm pull-xs-right {{ if .Article.IsFavorited }} active {{ end }}" + hx-post="/htmx/users/articles/{{ .Article.Slug }}/favorite" + + {{ if .IsSelf }} + hx-swap="delete" + hx-target="closest .post-preview" + {{ else }} + hx-swap="outerHTML" + {{ end }} +> + <i class="ion-heart"></i> {{ .Article.GetFavoriteCount }} +</button>
\ No newline at end of file diff --git a/cmd/web/templates/users/partials/feed-navigation.tmpl b/cmd/web/templates/users/partials/feed-navigation.tmpl new file mode 100644 index 0000000..00629d9 --- /dev/null +++ b/cmd/web/templates/users/partials/feed-navigation.tmpl @@ -0,0 +1,17 @@ +<ul id="user-feed-navigation" class="nav nav-pills outline-active" hx-swap-oob="true"> + {{ range $item := .FeedNavbarItems }} + <li class="nav-item"> + <a class="nav-link {{ if $item.IsActive }} active {{ end }}" + {{ if not $item.IsActive }} + href="{{ $item.HXPushURL }}" + hx-get="{{ $item.HXGetURL }}" + hx-trigger="click" + hx-target="#user-post-preview" + hx-push-url="{{ $item.HXPushURL }}" + {{ end }} + > + {{ $item.Title }} + </a> + </li> + {{ end }} +</ul>
\ No newline at end of file diff --git a/cmd/web/templates/users/partials/follow-button.tmpl b/cmd/web/templates/users/partials/follow-button.tmpl new file mode 100644 index 0000000..d893ea0 --- /dev/null +++ b/cmd/web/templates/users/partials/follow-button.tmpl @@ -0,0 +1,14 @@ +<button class="btn btn-sm btn-outline-secondary follow-button action-btn" + hx-post="/htmx/users/{{ .User.Username }}/follow" + hx-swap="outerHTML" +> + {{ if .IsFollowed }} + <i class="ion-minus-round"></i> + Unfollow + {{ else }} + <i class="ion-plus-round"></i> + Follow + {{ end }} + {{ .User.Name }} + <span class="counter">({{ .User.FollowersCount }})</span> +</button>
\ No newline at end of file diff --git a/cmd/web/templates/users/partials/post-preview.tmpl b/cmd/web/templates/users/partials/post-preview.tmpl new file mode 100644 index 0000000..4e00357 --- /dev/null +++ b/cmd/web/templates/users/partials/post-preview.tmpl @@ -0,0 +1,61 @@ +<div id="user-post-preview"> + {{ if .HasArticles }} + {{ $isSelf := .IsSelf }} + {{ range $article := .Articles }} + + <div class="post-preview"> + <div class="post-meta"> + <a href="/users/{{ $article.User.Username }}" + hx-push-url="/users/{{ $article.User.Username }}" + hx-get="/htmx/users/{{ $article.User.Username }}" + hx-target="#app-body" + > + <img src="{{ $article.User.Image }}" /> + </a> + + <div class="info"> + <a href="/users/{{ $article.User.Username }}" + hx-push-url="/users/{{ $article.User.Username }}" + hx-get="/htmx/users/{{ $article.User.Username }}" + hx-target="#app-body" + class="author" + > + {{ $article.User.Name }} + </a> + <span class="date">{{ $article.GetFormattedCreatedAt }}</span> + </div> + + {{ template "users/partials/article-favorite-button" Dict "Article" $article "IsSelf" $isSelf }} + + </div> + <a href="/articles/{{ $article.Slug }}" + hx-push-url="/articles/{{ $article.Slug }}" + hx-get="/htmx/articles/{{ $article.Slug }}" + hx-target="#app-body" + class="preview-link" + > + <h1>{{ $article.Title }}</h1> + <p>{{ $article.Description }}</p> + + <div class="m-t-1"> + <span>Read more...</span> + + <ul class="tag-list"> + {{ range $tag := $article.Tags }} + <li class="tag-default tag-pill tag-outline">{{ $tag.Name }}</li> + {{ end }} + </ul> + </div> + </a> + </div> + {{ end }} + {{ end }} + + {{ if not .HasArticles }} + <div class="post-preview"> + <div class="alert alert-warning" role="alert"> + No articles are here... yet. + </div> + </div> + {{ end }} +</div>
\ No newline at end of file diff --git a/cmd/web/templates/users/show.tmpl b/cmd/web/templates/users/show.tmpl new file mode 100644 index 0000000..cf9f1d7 --- /dev/null +++ b/cmd/web/templates/users/show.tmpl @@ -0,0 +1,51 @@ +<div class="profile-page"> + <div class="user-info"> + <div class="container"> + <div class="row"> + + <div class="col-md-10 col-md-offset-1"> + <img src="{{ .User.Image }}" class="user-img" /> + <h4>{{ .User.Name }}</h4> + <p>{{ .User.Bio }}</p> + + {{ if .IsSelf }} + <a class="btn btn-sm btn-outline-secondary action-btn" + href="/settings" + hx-push-url="/settings" + hx-get="/htmx/settings" + hx-target="#app-body" + > + <i class="ion-ios-gear"></i> + + Edit Profile Settings</span> + </a> + {{ else }} + + {{ template "users/partials/follow-button" . }} + + {{ end }} + </div> + + </div> + </div> + </div> + + <div class="container"> + <div class="row"> + <div class="col-md-10 col-md-offset-1"> + <div class="posts-toggle"> + <ul id="user-feed-navigation" class="nav nav-pills outline-active"></ul> + </div> + + <div id="user-post-preview" + {{ if .IsLoadFavorites }} + hx-get="/htmx/users/{{ .User.Username }}/favorites" + {{ else }} + hx-get="/htmx/users/{{ .User.Username }}/articles" + {{ end }} + hx-trigger="load" + ></div> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/database.sqlite b/database.sqlite Binary files differnew file mode 100644 index 0000000..1acbe46 --- /dev/null +++ b/database.sqlite diff --git a/fiber.sqlite3 b/fiber.sqlite3 Binary files differnew file mode 100644 index 0000000..4c93eae --- /dev/null +++ b/fiber.sqlite3 @@ -0,0 +1,50 @@ +module projecty + +go 1.21.1 + +require ( + github.com/glebarez/sqlite v1.9.0 + github.com/go-playground/validator/v10 v10.15.5 + github.com/gofiber/fiber/v2 v2.50.0 + github.com/gofiber/storage/mysql v1.3.7 + github.com/gofiber/template/html/v2 v2.0.5 + github.com/gosimple/slug v1.13.1 + golang.org/x/crypto v0.14.0 + gorm.io/gorm v1.25.4 +) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/gofiber/storage/sqlite3 v1.3.8 // indirect + github.com/gofiber/template v1.8.2 // indirect + github.com/gofiber/utils v1.1.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + modernc.org/libc v1.24.1 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.6.0 // indirect + modernc.org/sqlite v1.26.0 // indirect +) @@ -0,0 +1,105 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= +github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= +github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= +github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= +github.com/gofiber/storage/mysql v1.3.7 h1:yGYI7nnzaTE4o+nu3I4B1bL7KBTOolCjQMjCiWmdqz0= +github.com/gofiber/storage/mysql v1.3.7/go.mod h1:I4oWq/j6rp2mXkQwzQMJLhXLKupJzAJLlDDf5zm0ETo= +github.com/gofiber/storage/sqlite3 v1.3.8 h1:ywicq0MvlO4H+IbxwvSq3GvTv25fmhEZ1LpEkd8b078= +github.com/gofiber/storage/sqlite3 v1.3.8/go.mod h1:G4A9R3Ac2G9Wpb76F62oEqXUTb0ywjTIr5P7obiZmYc= +github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk= +github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= +github.com/gofiber/template/html/v2 v2.0.5 h1:BKLJ6Qr940NjntbGmpO3zVa4nFNGDCi/IfUiDB9OC20= +github.com/gofiber/template/html/v2 v2.0.5/go.mod h1:RCF14eLeQDCSUPp0IGc2wbSSDv6yt+V54XB/+Unz+LM= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= +github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= +modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= +modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw= +modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= diff --git a/internal/authentication/session.go b/internal/authentication/session.go new file mode 100644 index 0000000..8204034 --- /dev/null +++ b/internal/authentication/session.go @@ -0,0 +1,60 @@ +package authentication + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/storage/sqlite3" +) + +var StoredAuthenticationSession *session.Store + +func SessionStart() { + + store := sqlite3.New(sqlite3.Config{ + Table: "fiber_storage", + }) + + authSession := session.New(session.Config{ + Storage: store, + }) + + StoredAuthenticationSession = authSession +} + +func AuthStore(c *fiber.Ctx, userID uint) { + session, err := StoredAuthenticationSession.Get(c) + if err != nil { + panic(err) + } + + session.Set("authentication", userID) + if err := session.Save(); err != nil { + panic(err) + } +} + +func AuthGet(c *fiber.Ctx) (bool, uint) { + session, err := StoredAuthenticationSession.Get(c) + if err != nil { + panic(err) + } + + value := session.Get("authentication") + if value == nil { + return false, 0 + } + + return true, value.(uint) +} + +func AuthDestroy(c *fiber.Ctx) { + session, err := StoredAuthenticationSession.Get(c) + if err != nil { + panic(err) + } + + session.Delete("authentication") + if err := session.Save(); err != nil { + panic(err) + } +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..c67c75e --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,50 @@ +package database + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +func Open() { + + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Millisecond * 10, // Slow SQL threshold + LogLevel: logger.Info, // Log level + IgnoreRecordNotFoundError: false, // Ignore ErrRecordNotFound error for logger + Colorful: true, // Disable color + }, + ) + + db, err := gorm.Open(sqlite.Open("database.sqlite"), &gorm.Config{ + Logger: newLogger, + }) + + if err != nil { + fmt.Println("storage err: ", err) + } + + sqlDB, err := db.DB() + if err != nil { + fmt.Println("storage err: ", err) + } + + sqlDB.SetMaxIdleConns(3) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + DB = db +} + +func Get() *gorm.DB { + return DB +} diff --git a/internal/errormessage.go b/internal/errormessage.go new file mode 100644 index 0000000..34e0466 --- /dev/null +++ b/internal/errormessage.go @@ -0,0 +1,17 @@ +package internal + +func ErrorMessage(fieldName string, fieldType string) string { + + message := "" + + switch fieldType { + case "required": + message = fieldName + " is required." + case "email": + message = fieldName + " must be an email." + default: + message = "" + } + + return message +} diff --git a/internal/helper/route.go b/internal/helper/route.go new file mode 100644 index 0000000..69f00b9 --- /dev/null +++ b/internal/helper/route.go @@ -0,0 +1,15 @@ +package helper + +import "github.com/gofiber/fiber/v2" + +func HTMXRedirectTo(HXURL string, HXGETURL string, c *fiber.Ctx) error { + + c.Append("HX-Replace-Url", HXURL) + c.Append("HX-Reswap", "none") + + return c.Render("components/redirect", fiber.Map{ + "HXGet": HXGETURL, + "HXTarget": "#app-body", + "HXTrigger": "load", + }, "layouts/app-htmx") +} diff --git a/internal/middleware/NotFound.go b/internal/middleware/NotFound.go new file mode 100644 index 0000000..2db3198 --- /dev/null +++ b/internal/middleware/NotFound.go @@ -0,0 +1,10 @@ +package middleware + +import "github.com/gofiber/fiber/v2" + +func NotFound(c *fiber.Ctx) error { + + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "message": "Page not found.", + }) +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go new file mode 100644 index 0000000..92af85c --- /dev/null +++ b/internal/renderer/renderer.go @@ -0,0 +1,45 @@ +package renderer + +import ( + "errors" + "projecty/internal/authentication" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/html/v2" +) + +func ViewEngineStart() *html.Engine { + + viewEngine := html.New("./cmd/web/templates", ".tmpl") + + viewEngine.AddFunc("IsAuthenticated", func(c *fiber.Ctx) bool { + isAuthenticated, _ := authentication.AuthGet(c) + return isAuthenticated + }) + + viewEngine.AddFunc("Iterate", func(start int, end int) []int { + n := end - start + 1 + result := make([]int, n) + for i := 0; i < n; i++ { + result[i] = start + i + } + return result + }) + + viewEngine.AddFunc("Dict", func(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dict keys must be strings") + } + dict[key] = values[i+1] + } + return dict, nil + }) + + return viewEngine +} diff --git a/internal/validator.go b/internal/validator.go new file mode 100644 index 0000000..2d9550a --- /dev/null +++ b/internal/validator.go @@ -0,0 +1,17 @@ +package internal + +import "github.com/go-playground/validator/v10" + +type Validator struct { + validator *validator.Validate +} + +func NewValidator() *Validator { + return &Validator{ + validator: validator.New(), + } +} + +func (v *Validator) Validate(i interface{}) error { + return v.validator.Struct(i) +} @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "projecty/cmd/web" + "projecty/internal/authentication" + "projecty/internal/database" + "projecty/internal/renderer" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +var validate *validator.Validate + +func main() { + + viewEngine := renderer.ViewEngineStart() + app := fiber.New(fiber.Config{ + Views: viewEngine, + }) + + database.Open() + authentication.SessionStart() + + // Middleware + app.Use(recover.New()) + app.Use(logger.New()) + + web.Serve(app) + + log.Fatal(app.Listen("localhost:8181")) +} diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..05e5985 --- /dev/null +++ b/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1
\ No newline at end of file diff --git a/tmp/main b/tmp/main Binary files differnew file mode 100755 index 0000000..6cc0177 --- /dev/null +++ b/tmp/main |