Dima 1 rok pred
rodič
commit
d44fd9070c

+ 13 - 3
internal/helpers/datetime.go

@@ -3,15 +3,25 @@ package helpers
 import "time"
 import "time"
 
 
 const (
 const (
-	yearFormat  = "2006"
-	monthFormat = "01"
-	dayFormat   = "2"
+	dateTimeFormat = "2006-01-02"
+	yearFormat     = "2006"
+	monthFormat    = "01"
+	dateFormat     = "02"
+	dayFormat      = "2"
 )
 )
 
 
+func ParseDateTime(dateTime string) (time.Time, error) {
+	return time.Parse(dateTimeFormat, dateTime)
+}
+
 func FormatDateStr(date time.Time) string {
 func FormatDateStr(date time.Time) string {
 	return date.Format(dayFormat) + " " + getMonthStr(date.Format(monthFormat)) + " " + date.Format(yearFormat)
 	return date.Format(dayFormat) + " " + getMonthStr(date.Format(monthFormat)) + " " + date.Format(yearFormat)
 }
 }
 
 
+func FormatDateForm(date time.Time) string {
+	return date.Format(yearFormat) + "-" + date.Format(monthFormat) + "-" + date.Format(dateFormat)
+}
+
 func getMonthStr(month string) string {
 func getMonthStr(month string) string {
 	switch month {
 	switch month {
 	case "01":
 	case "01":

+ 10 - 6
internal/mapper/article.go

@@ -73,12 +73,12 @@ func ConvertArticleModelToForm(a models.Article, tags []models.Tag) *models.Arti
 		PreviewText:     a.PreviewText.String,
 		PreviewText:     a.PreviewText.String,
 		MetaKeywords:    a.MetaKeywords.String,
 		MetaKeywords:    a.MetaKeywords.String,
 		MetaDescription: a.MetaDescription.String,
 		MetaDescription: a.MetaDescription.String,
-		PublishTime:     a.PublishTime,
+		PublishTime:     helpers.FormatDateForm(a.PublishTime),
 		ActiveTags:      tagMap,
 		ActiveTags:      tagMap,
 	}
 	}
 }
 }
 
 
-func ConvertArticleFormToModel(f models.ArticleForm) models.Article {
+func ConvertArticleFormToModel(f models.ArticleForm) (*models.Article, error) {
 	var previewText, image, metaKeywords, metaDesc sql.NullString
 	var previewText, image, metaKeywords, metaDesc sql.NullString
 
 
 	if f.PreviewText != "" {
 	if f.PreviewText != "" {
@@ -109,17 +109,21 @@ func ConvertArticleFormToModel(f models.ArticleForm) models.Article {
 		}
 		}
 	}
 	}
 
 
-	return models.Article{
+	parsedDateTime, err := helpers.ParseDateTime(f.PublishTime)
+	if err != nil {
+		return nil, err
+	}
+
+	return &models.Article{
 		ID:              f.ID,
 		ID:              f.ID,
 		URL:             f.URL,
 		URL:             f.URL,
 		Title:           f.Title,
 		Title:           f.Title,
-		PublishTime:     f.PublishTime,
+		PublishTime:     parsedDateTime,
 		Text:            f.Text,
 		Text:            f.Text,
 		PreviewText:     previewText,
 		PreviewText:     previewText,
 		IsActive:        f.IsActive,
 		IsActive:        f.IsActive,
 		Image:           image,
 		Image:           image,
 		MetaKeywords:    metaKeywords,
 		MetaKeywords:    metaKeywords,
 		MetaDescription: metaDesc,
 		MetaDescription: metaDesc,
-	}
-
+	}, nil
 }
 }

+ 10 - 10
internal/models/article.go

@@ -29,15 +29,15 @@ type Article struct {
 
 
 type ArticleForm struct {
 type ArticleForm struct {
 	ID              int
 	ID              int
-	Title           string    `form:"title" validate:"required,max=255"`
-	Image           string    `form:"image" validate:"uri,max=255"`
-	URL             string    `form:"url" validate:"required,max=255"`
-	Text            string    `form:"text" validate:"required"`
-	PreviewText     string    `form:"preview_text" validate:"max=255"`
-	MetaKeywords    string    `form:"meta_keywords" validate:"max=255"`
-	MetaDescription string    `form:"meta_description" validate:"max=255"`
-	IsActive        bool      `form:"is_active"`
-	PublishTime     time.Time `form:"publish_time" validate:"required"`
-	Tags            []string  `form:"tag"`
+	Title           string   `form:"title" validate:"required,max=255"`
+	Image           string   `form:"image" validate:"uri,max=255"`
+	URL             string   `form:"url" validate:"required,max=255"`
+	Text            string   `form:"text" validate:"required"`
+	PreviewText     string   `form:"preview_text" validate:"max=255"`
+	MetaKeywords    string   `form:"meta_keywords" validate:"max=255"`
+	MetaDescription string   `form:"meta_description" validate:"max=255"`
+	IsActive        bool     `form:"is_active"`
+	PublishTime     string   `form:"publish_time" validate:"required"`
+	Tags            []string `form:"tag"`
 	ActiveTags      map[int]bool
 	ActiveTags      map[int]bool
 }
 }

+ 12 - 4
internal/repositories/tag.go

@@ -144,7 +144,7 @@ func (t tagRepository) GetByArticleID(ctx context.Context, id int) ([]models.Tag
 
 
 	query := "SELECT t.id, t.url, t.tag " +
 	query := "SELECT t.id, t.url, t.tag " +
 		"FROM " + articleTagTableName + " at, " + tagTableName + " t " +
 		"FROM " + articleTagTableName + " at, " + tagTableName + " t " +
-		"WHERE t.id = at.tag_id AND at.article_id = ?"
+		"WHERE t.id = at.tag_id AND at.article_id = $1"
 
 
 	rows, err := t.db.QueryContext(ctx, query, id)
 	rows, err := t.db.QueryContext(ctx, query, id)
 	if err != nil {
 	if err != nil {
@@ -171,11 +171,19 @@ func (t tagRepository) GetByArticleID(ctx context.Context, id int) ([]models.Tag
 }
 }
 
 
 func (t tagRepository) IsUsed(ctx context.Context, id int) (bool, error) {
 func (t tagRepository) IsUsed(ctx context.Context, id int) (bool, error) {
-	var count int
+	query, args, err := sq.Select("COUNT(tag_id)").
+		From(articleTagTableName).
+		PlaceholderFormat(sq.Dollar).
+		Where(sq.Eq{"tag_id": id}).
+		ToSql()
 
 
-	query := "SELECT COUNT(tag_id) FROM " + articleTagTableName + " WHERE tag_id = ?"
+	if err != nil {
+		return false, err
+	}
 
 
-	if err := t.db.QueryRowContext(ctx, query, id).Scan(&count); err != nil {
+	var count int
+	err = t.db.QueryRowContext(ctx, query, args...).Scan(&count)
+	if err != nil {
 		return false, err
 		return false, err
 	}
 	}
 
 

+ 45 - 38
internal/services/handler/admin/article.go

@@ -28,7 +28,8 @@ func ArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 }
 }
 
 
 func AddArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func AddArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
 		var validate = validator.New()
 		var validate = validator.New()
 		validateErrors := make(map[string]string)
 		validateErrors := make(map[string]string)
 
 
@@ -41,15 +42,15 @@ func AddArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			ActiveTags: make(map[int]bool),
 			ActiveTags: make(map[int]bool),
 		}
 		}
 
 
-		tags, err := sp.GetTagRepository().GetAll(ctx.Context())
+		tags, err := sp.GetTagRepository().GetAll(ctx)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
 		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 
-		if ctx.Method() == fiber.MethodPost {
-			if err = ctx.BodyParser(&form); err != nil {
+		if fctx.Method() == fiber.MethodPost {
+			if err = fctx.BodyParser(&form); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -57,7 +58,7 @@ func AddArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 			}
 			}
 
 
-			if res, _ := sp.GetArticleRepository().GetByURL(ctx.Context(), form.URL); res != nil {
+			if res, _ := sp.GetArticleRepository().GetByURL(ctx, form.URL); res != nil {
 				validateErrors["ArticleForm.URL"] = errArticleExists
 				validateErrors["ArticleForm.URL"] = errArticleExists
 			}
 			}
 
 
@@ -76,26 +77,29 @@ func AddArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			}
 			}
 
 
 			if len(validateErrors) == 0 {
 			if len(validateErrors) == 0 {
-				articleID, articleErr := sp.GetArticleRepository().Add(ctx.Context(), mapper.ConvertArticleFormToModel(form))
+				articleModel, err := mapper.ConvertArticleFormToModel(form)
+				if err != nil {
+					return err
+				}
+
+				articleID, articleErr := sp.GetArticleRepository().Add(ctx, *articleModel)
 				if articleErr != nil {
 				if articleErr != nil {
 					return articleErr
 					return articleErr
 				}
 				}
 
 
 				if len(form.Tags) > 0 {
 				if len(form.Tags) > 0 {
-					if err = sp.GetArticleTagRepository().Add(ctx.Context(), articleID, tagIDs); err != nil {
+					if err = sp.GetArticleTagRepository().Add(ctx, articleID, tagIDs); err != nil {
 						return err
 						return err
 					}
 					}
 				}
 				}
 
 
 				sp.GetCacheService().FlushAll()
 				sp.GetCacheService().FlushAll()
 
 
-				if err = ctx.Redirect("/admin"); err != nil {
-					return err
-				}
+				return fctx.Redirect("/admin")
 			}
 			}
 		}
 		}
 
 
-		return ctx.Render("admin/article_edit", fiber.Map{
+		return fctx.Render("admin/article_edit", fiber.Map{
 			"form":       form,
 			"form":       form,
 			"errors":     validateErrors,
 			"errors":     validateErrors,
 			"tags":       tagsDTO,
 			"tags":       tagsDTO,
@@ -107,7 +111,8 @@ func AddArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 }
 }
 
 
 func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
 		var validate = validator.New()
 		var validate = validator.New()
 		validateErrors := make(map[string]string)
 		validateErrors := make(map[string]string)
 
 
@@ -116,12 +121,12 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			return err
 			return err
 		}
 		}
 
 
-		ID, err := strconv.Atoi(ctx.Params("id"))
+		ID, err := strconv.Atoi(fctx.Params("id"))
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		article, err := sp.GetArticleRepository().GetByID(ctx.Context(), ID)
+		article, err := sp.GetArticleRepository().GetByID(ctx, ID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -129,12 +134,12 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			return fiber.ErrNotFound
 			return fiber.ErrNotFound
 		}
 		}
 
 
-		articleTags, err := sp.GetTagRepository().GetByArticleID(ctx.Context(), ID)
+		articleTags, err := sp.GetTagRepository().GetByArticleID(ctx, ID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		tags, err := sp.GetTagRepository().GetAll(ctx.Context())
+		tags, err := sp.GetTagRepository().GetAll(ctx)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -142,15 +147,15 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 
 		var form *models.ArticleForm
 		var form *models.ArticleForm
-		if ctx.Method() == fiber.MethodGet {
+		if fctx.Method() == fiber.MethodGet {
 			form = mapper.ConvertArticleModelToForm(*article, articleTags)
 			form = mapper.ConvertArticleModelToForm(*article, articleTags)
-		} else if ctx.Method() == fiber.MethodPost {
+		} else if fctx.Method() == fiber.MethodPost {
 			form = &models.ArticleForm{
 			form = &models.ArticleForm{
 				ID:         ID,
 				ID:         ID,
 				ActiveTags: make(map[int]bool),
 				ActiveTags: make(map[int]bool),
 			}
 			}
 
 
-			if err = ctx.BodyParser(form); err != nil {
+			if err = fctx.BodyParser(form); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -158,7 +163,7 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 			}
 			}
 
 
-			if res, _ := sp.GetArticleRepository().GetByURL(ctx.Context(), form.URL); res != nil {
+			if res, _ := sp.GetArticleRepository().GetByURL(ctx, form.URL); res != nil {
 				if res.ID != ID {
 				if res.ID != ID {
 					validateErrors["ArticleForm.URL"] = errArticleExists
 					validateErrors["ArticleForm.URL"] = errArticleExists
 				}
 				}
@@ -179,7 +184,12 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			}
 			}
 
 
 			if len(validateErrors) == 0 {
 			if len(validateErrors) == 0 {
-				err = sp.GetArticleRepository().Update(ctx.Context(), mapper.ConvertArticleFormToModel(*form))
+				articleModel, err := mapper.ConvertArticleFormToModel(*form)
+				if err != nil {
+					return err
+				}
+
+				err = sp.GetArticleRepository().Update(ctx, *articleModel)
 				if err != nil {
 				if err != nil {
 					return err
 					return err
 				}
 				}
@@ -201,28 +211,26 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 				}
 				}
 
 
 				if len(tagsToAdd) > 0 {
 				if len(tagsToAdd) > 0 {
-					if err = sp.GetArticleTagRepository().Add(ctx.Context(), ID, tagsToAdd); err != nil {
+					if err = sp.GetArticleTagRepository().Add(ctx, ID, tagsToAdd); err != nil {
 						return err
 						return err
 					}
 					}
 				}
 				}
 
 
 				if len(tagsToDelete) > 0 {
 				if len(tagsToDelete) > 0 {
-					if err = sp.GetArticleTagRepository().Delete(ctx.Context(), ID, tagsToDelete); err != nil {
+					if err = sp.GetArticleTagRepository().Delete(ctx, ID, tagsToDelete); err != nil {
 						return err
 						return err
 					}
 					}
 				}
 				}
 
 
 				sp.GetCacheService().FlushAll()
 				sp.GetCacheService().FlushAll()
 
 
-				if ctx.FormValue("action", "save") == "save" {
-					if err = ctx.Redirect("/admin"); err != nil {
-						return err
-					}
+				if fctx.FormValue("action", "save") == "save" {
+					return fctx.Redirect("/admin")
 				}
 				}
 			}
 			}
 		}
 		}
 
 
-		return ctx.Render("admin/article_edit", fiber.Map{
+		return fctx.Render("admin/article_edit", fiber.Map{
 			"form":       form,
 			"form":       form,
 			"errors":     validateErrors,
 			"errors":     validateErrors,
 			"tags":       tagsDTO,
 			"tags":       tagsDTO,
@@ -234,36 +242,35 @@ func EditArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 }
 }
 
 
 func DeleteArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func DeleteArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
-		ID, err := strconv.Atoi(ctx.Params("id"))
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
+		ID, err := strconv.Atoi(fctx.Params("id"))
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		article, err := sp.GetArticleRepository().GetByID(ctx.Context(), ID)
+		article, err := sp.GetArticleRepository().GetByID(ctx, ID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		if ctx.Method() == fiber.MethodPost {
-			err = sp.GetArticleTagRepository().DeleteByArticleID(ctx.Context(), ID)
+		if fctx.Method() == fiber.MethodPost {
+			err = sp.GetArticleTagRepository().DeleteByArticleID(ctx, ID)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 
 
-			err = sp.GetArticleRepository().Delete(ctx.Context(), ID)
+			err = sp.GetArticleRepository().Delete(ctx, ID)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 
 
 			sp.GetCacheService().FlushAll()
 			sp.GetCacheService().FlushAll()
 
 
-			if err = ctx.Redirect("/admin"); err != nil {
-				return err
-			}
+			return fctx.Redirect("/admin")
 		}
 		}
 
 
-		return ctx.Render("admin/article_delete", fiber.Map{
+		return fctx.Render("admin/article_delete", fiber.Map{
 			"article": article.Title,
 			"article": article.Title,
 			"section": "article",
 			"section": "article",
 		}, "admin/_layout")
 		}, "admin/_layout")

+ 31 - 36
internal/services/handler/admin/tag.go

@@ -14,23 +14,22 @@ import (
 const errTagExists = "Тег с данным URL уже существует"
 const errTagExists = "Тег с данным URL уже существует"
 
 
 func TagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func TagHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
-		tags, err := sp.GetTagRepository().GetAll(ctx.Context())
+	return func(fctx *fiber.Ctx) error {
+		tags, err := sp.GetTagRepository().GetAll(fctx.Context())
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		tagsDTO := mapper.ConvertTagModelsToDTO(tags)
-
-		return ctx.Render("admin/tag", fiber.Map{
-			"tags":    tagsDTO,
+		return fctx.Render("admin/tag", fiber.Map{
+			"tags":    mapper.ConvertTagModelsToDTO(tags),
 			"section": "tag",
 			"section": "tag",
 		}, "admin/_layout")
 		}, "admin/_layout")
 	}
 	}
 }
 }
 
 
 func AddTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func AddTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
 		var form models.TagForm
 		var form models.TagForm
 		var validate = validator.New()
 		var validate = validator.New()
 		validateErrors := make(map[string]string)
 		validateErrors := make(map[string]string)
@@ -40,8 +39,8 @@ func AddTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			return err
 			return err
 		}
 		}
 
 
-		if ctx.Method() == fiber.MethodPost {
-			if err = ctx.BodyParser(&form); err != nil {
+		if fctx.Method() == fiber.MethodPost {
+			if err = fctx.BodyParser(&form); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -49,25 +48,23 @@ func AddTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 			}
 			}
 
 
-			if res, _ := sp.GetTagRepository().GetByURL(ctx.Context(), form.URL); res != nil {
+			if res, _ := sp.GetTagRepository().GetByURL(ctx, form.URL); res != nil {
 				validateErrors["TagForm.URL"] = errTagExists
 				validateErrors["TagForm.URL"] = errTagExists
 			}
 			}
 
 
 			if len(validateErrors) == 0 {
 			if len(validateErrors) == 0 {
-				err = sp.GetTagRepository().Add(ctx.Context(), mapper.ConvertTagFormToModel(form))
+				err = sp.GetTagRepository().Add(ctx, mapper.ConvertTagFormToModel(form))
 				if err != nil {
 				if err != nil {
 					return err
 					return err
 				}
 				}
 
 
 				sp.GetCacheService().FlushAll()
 				sp.GetCacheService().FlushAll()
 
 
-				if err = ctx.Redirect("/admin/tag"); err != nil {
-					return err
-				}
+				return fctx.Redirect("/admin/tag")
 			}
 			}
 		}
 		}
 
 
-		return ctx.Render("admin/tag_edit", fiber.Map{
+		return fctx.Render("admin/tag_edit", fiber.Map{
 			"form":    form,
 			"form":    form,
 			"errors":  validateErrors,
 			"errors":  validateErrors,
 			"section": "tag",
 			"section": "tag",
@@ -77,7 +74,8 @@ func AddTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 }
 }
 
 
 func EditTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func EditTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
 		var validate = validator.New()
 		var validate = validator.New()
 		validateErrors := make(map[string]string)
 		validateErrors := make(map[string]string)
 
 
@@ -86,12 +84,12 @@ func EditTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			return err
 			return err
 		}
 		}
 
 
-		ID, err := strconv.Atoi(ctx.Params("id"))
+		ID, err := strconv.Atoi(fctx.Params("id"))
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		tag, err := sp.GetTagRepository().GetByID(ctx.Context(), ID)
+		tag, err := sp.GetTagRepository().GetByID(ctx, ID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -102,8 +100,8 @@ func EditTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			URL: tag.URL,
 			URL: tag.URL,
 		}
 		}
 
 
-		if ctx.Method() == fiber.MethodPost {
-			if err = ctx.BodyParser(&form); err != nil {
+		if fctx.Method() == fiber.MethodPost {
+			if err = fctx.BodyParser(&form); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -111,27 +109,25 @@ func EditTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 				validateErrors = helpers.FormatValidateErrors(err, trans)
 			}
 			}
 
 
-			if res, _ := sp.GetTagRepository().GetByURL(ctx.Context(), form.URL); res != nil {
+			if res, _ := sp.GetTagRepository().GetByURL(ctx, form.URL); res != nil {
 				if res.ID != ID {
 				if res.ID != ID {
 					validateErrors["TagForm.URL"] = errTagExists
 					validateErrors["TagForm.URL"] = errTagExists
 				}
 				}
 			}
 			}
 
 
 			if len(validateErrors) == 0 {
 			if len(validateErrors) == 0 {
-				err = sp.GetTagRepository().Update(ctx.Context(), mapper.ConvertTagFormToModel(form))
+				err = sp.GetTagRepository().Update(ctx, mapper.ConvertTagFormToModel(form))
 				if err != nil {
 				if err != nil {
 					return err
 					return err
 				}
 				}
 
 
 				sp.GetCacheService().FlushAll()
 				sp.GetCacheService().FlushAll()
 
 
-				if err = ctx.Redirect("/admin/tag"); err != nil {
-					return err
-				}
+				return fctx.Redirect("/admin/tag")
 			}
 			}
 		}
 		}
 
 
-		return ctx.Render("admin/tag_edit", fiber.Map{
+		return fctx.Render("admin/tag_edit", fiber.Map{
 			"form":    form,
 			"form":    form,
 			"errors":  validateErrors,
 			"errors":  validateErrors,
 			"section": "tag",
 			"section": "tag",
@@ -141,36 +137,35 @@ func EditTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 }
 }
 
 
 func DeleteTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func DeleteTagHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
-		ID, err := strconv.Atoi(ctx.Params("id"))
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
+		ID, err := strconv.Atoi(fctx.Params("id"))
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		tag, err := sp.GetTagRepository().GetByID(ctx.Context(), ID)
+		tag, err := sp.GetTagRepository().GetByID(ctx, ID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		used, err := sp.GetTagRepository().IsUsed(ctx.Context(), ID)
+		used, err := sp.GetTagRepository().IsUsed(ctx, ID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		if ctx.Method() == fiber.MethodPost {
-			err = sp.GetTagRepository().Delete(ctx.Context(), ID)
+		if fctx.Method() == fiber.MethodPost {
+			err = sp.GetTagRepository().Delete(ctx, ID)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 
 
 			sp.GetCacheService().FlushAll()
 			sp.GetCacheService().FlushAll()
 
 
-			if err = ctx.Redirect("/admin/tag"); err != nil {
-				return err
-			}
+			return fctx.Redirect("/admin/tag")
 		}
 		}
 
 
-		return ctx.Render("admin/tag_delete", fiber.Map{
+		return fctx.Render("admin/tag_delete", fiber.Map{
 			"tag":     tag.Tag,
 			"tag":     tag.Tag,
 			"used":    used,
 			"used":    used,
 			"section": "tag",
 			"section": "tag",

+ 7 - 6
internal/services/handler/article.go

@@ -15,13 +15,14 @@ const (
 )
 )
 
 
 func ArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func ArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
-		articleReq := ctx.Params(articleParam)
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
+		articleReq := fctx.Params(articleParam)
 
 
 		renderData, found := sp.GetCacheService().Get(articleCacheKey + articleReq)
 		renderData, found := sp.GetCacheService().Get(articleCacheKey + articleReq)
 
 
 		if !found {
 		if !found {
-			article, err := sp.GetArticleRepository().GetByURL(ctx.Context(), articleReq)
+			article, err := sp.GetArticleRepository().GetByURL(ctx, articleReq)
 			if err != nil {
 			if err != nil {
 				if err == sql.ErrNoRows {
 				if err == sql.ErrNoRows {
 					return fiber.ErrNotFound
 					return fiber.ErrNotFound
@@ -36,7 +37,7 @@ func ArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			articleDTO := mapper.ConvertArticleModelToDTO(*article)
 			articleDTO := mapper.ConvertArticleModelToDTO(*article)
 
 
 			// All used tags
 			// All used tags
-			tags, err := sp.GetTagRepository().GetAllUsed(ctx.Context())
+			tags, err := sp.GetTagRepository().GetAllUsed(ctx)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -44,7 +45,7 @@ func ArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 			tagsDTO := mapper.ConvertTagModelsToDTO(tags)
 
 
 			// Last articles
 			// Last articles
-			articles, err := sp.GetArticleRepository().GetAllPreview(ctx.Context())
+			articles, err := sp.GetArticleRepository().GetAllPreview(ctx)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -65,6 +66,6 @@ func ArticleHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			sp.GetCacheService().Set(articleCacheKey+articleReq, renderData)
 			sp.GetCacheService().Set(articleCacheKey+articleReq, renderData)
 		}
 		}
 
 
-		return ctx.Render("article", renderData, "_layout")
+		return fctx.Render("article", renderData, "_layout")
 	}
 	}
 }
 }

+ 3 - 3
internal/services/handler/main_page.go

@@ -9,11 +9,11 @@ import (
 const allPreviewArticlesCacheKey = "all-preview-articles"
 const allPreviewArticlesCacheKey = "all-preview-articles"
 
 
 func MainPageHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func MainPageHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
+	return func(fctx *fiber.Ctx) error {
 		renderData, found := sp.GetCacheService().Get(allPreviewArticlesCacheKey)
 		renderData, found := sp.GetCacheService().Get(allPreviewArticlesCacheKey)
 
 
 		if !found {
 		if !found {
-			articles, err := sp.GetArticleRepository().GetAllPreview(ctx.Context())
+			articles, err := sp.GetArticleRepository().GetAllPreview(fctx.Context())
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -29,6 +29,6 @@ func MainPageHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			sp.GetCacheService().Set(allPreviewArticlesCacheKey, renderData)
 			sp.GetCacheService().Set(allPreviewArticlesCacheKey, renderData)
 		}
 		}
 
 
-		return ctx.Render("index", renderData, "_layout")
+		return fctx.Render("index", renderData, "_layout")
 	}
 	}
 }
 }

+ 6 - 5
internal/services/handler/tag.go

@@ -12,13 +12,14 @@ const tagParam = "tag"
 const tagCacheKey = "tag"
 const tagCacheKey = "tag"
 
 
 func TagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 func TagHandler(sp interfaces.ServiceProvider) fiber.Handler {
-	return func(ctx *fiber.Ctx) error {
-		tagReq := ctx.Params(tagParam)
+	return func(fctx *fiber.Ctx) error {
+		ctx := fctx.Context()
+		tagReq := fctx.Params(tagParam)
 
 
 		renderData, found := sp.GetCacheService().Get(tagCacheKey + tagReq)
 		renderData, found := sp.GetCacheService().Get(tagCacheKey + tagReq)
 
 
 		if !found {
 		if !found {
-			tag, err := sp.GetTagRepository().GetByURL(ctx.Context(), tagReq)
+			tag, err := sp.GetTagRepository().GetByURL(ctx, tagReq)
 			if err != nil {
 			if err != nil {
 				if err == sql.ErrNoRows {
 				if err == sql.ErrNoRows {
 					return fiber.ErrNotFound
 					return fiber.ErrNotFound
@@ -26,7 +27,7 @@ func TagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 				return err
 				return err
 			}
 			}
 
 
-			articles, err := sp.GetArticleRepository().GetPreviewByTagID(ctx.Context(), tag.ID)
+			articles, err := sp.GetArticleRepository().GetPreviewByTagID(ctx, tag.ID)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -42,6 +43,6 @@ func TagHandler(sp interfaces.ServiceProvider) fiber.Handler {
 			sp.GetCacheService().Set(tagCacheKey+tagReq, renderData)
 			sp.GetCacheService().Set(tagCacheKey+tagReq, renderData)
 		}
 		}
 
 
-		return ctx.Render("tag", renderData, "_layout")
+		return fctx.Render("tag", renderData, "_layout")
 	}
 	}
 }
 }

+ 1 - 12
internal/templates/admin/_layout.html

@@ -9,15 +9,9 @@
     <link href="/static/admin/css/dashboard.css?v={{ $v }}" rel="stylesheet">
     <link href="/static/admin/css/dashboard.css?v={{ $v }}" rel="stylesheet">
 </head>
 </head>
 <body>
 <body>
-<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
-    <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="/" target="_blank">dmitriygnatenko.ru</a>
-    <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
-        <span class="navbar-toggler-icon"></span>
-    </button>
-</header>
 <div class="container-fluid">
 <div class="container-fluid">
     <div class="row">
     <div class="row">
-        <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
+        <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block sidebar collapse bg-light">
             <div class="position-sticky pt-3 sidebar-sticky">
             <div class="position-sticky pt-3 sidebar-sticky">
                 <ul class="nav flex-column">
                 <ul class="nav flex-column">
                     <li class="nav-item">
                     <li class="nav-item">
@@ -30,11 +24,6 @@
                             Теги
                             Теги
                         </a>
                         </a>
                     </li>
                     </li>
-                    <li class="nav-item">
-                        <a class="nav-link" href="/admin/metrics" target="_blank">
-                           Метрики
-                        </a>
-                    </li>
                 </ul>
                 </ul>
             </div>
             </div>
         </nav>
         </nav>

+ 2 - 2
internal/templates/admin/article.html

@@ -2,7 +2,7 @@
     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
         <h3 class="h3">Статьи</h3>
         <h3 class="h3">Статьи</h3>
         <div class="btn-toolbar mb-2 mb-md-0">
         <div class="btn-toolbar mb-2 mb-md-0">
-            <a class="btn btn-sm btn-primary" href="/admin/article/add" title="Добавить">
+            <a class="btn btn-sm btn-success" href="/admin/article/add" title="Добавить">
                 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
                 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
                     <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
                     <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
                     <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
                     <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
@@ -32,7 +32,7 @@
                             <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
                             <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
                         </svg>
                         </svg>
                     </a>
                     </a>
-                    <a href="/admin/article/delete/{{ .ID }}" class="btn btn-sm btn-primary" title="Удалить">
+                    <a href="/admin/article/delete/{{ .ID }}" class="btn btn-sm btn-danger" title="Удалить">
                         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                             <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
                             <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
                             <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
                             <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>

+ 2 - 2
internal/templates/admin/tag.html

@@ -2,7 +2,7 @@
     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
         <h3 class="h3">Теги</h3>
         <h3 class="h3">Теги</h3>
         <div class="btn-toolbar mb-2 mb-md-0">
         <div class="btn-toolbar mb-2 mb-md-0">
-            <a class="btn btn-sm btn-primary" href="/admin/tag/add" title="Добавить">
+            <a class="btn btn-sm btn-success" href="/admin/tag/add" title="Добавить">
                 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
                 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
                     <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
                     <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
                     <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
                     <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
@@ -32,7 +32,7 @@
                             <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
                             <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
                         </svg>
                         </svg>
                     </a>
                     </a>
-                    <a href="/admin/tag/delete/{{ .ID }}" class="btn btn-sm btn-primary" title="Удалить">
+                    <a href="/admin/tag/delete/{{ .ID }}" class="btn btn-sm btn-danger" title="Удалить">
                         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                             <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
                             <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
                             <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
                             <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>

+ 1 - 5
web/static/admin/css/dashboard.css

@@ -14,14 +14,10 @@ body {
 .sidebar {
 .sidebar {
     position: fixed;
     position: fixed;
     top: 0;
     top: 0;
-    /* rtl:raw:
-    right: 0;
-    */
     bottom: 0;
     bottom: 0;
-    /* rtl:remove */
     left: 0;
     left: 0;
     z-index: 100; /* Behind the navbar */
     z-index: 100; /* Behind the navbar */
-    padding: 48px 0 0; /* Height of navbar */
+    padding: 0; /* Height of navbar */
     box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
     box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
 }
 }