Browse Source

Shop basic front-end template theme

Vova Tkach 5 years ago
parent
commit
5c1a3714d6

+ 19 - 0
assets/template/header_html_file.go

@@ -23,6 +23,14 @@ var VarHeaderHtmlFile = []byte(`<!doctype html>
 					{{else}}
 						Latest posts | Blog
 					{{end}}
+				{{else if or (eq $.Data.Module "shop") (eq $.Data.Module "shop-product") (eq $.Data.Module "shop-category")}}
+					{{if eq $.Data.Module "shop-category"}}
+						Products of category "{{$.Data.Shop.Category.Name}}" | Shop
+					{{else if eq $.Data.Module "shop-product"}}
+						{{$.Data.Shop.Product.Name}} | Shop
+					{{else}}
+						Latest products | Shop
+					{{end}}
 				{{end}}
 			{{else}}
 				Error 404
@@ -60,6 +68,9 @@ var VarHeaderHtmlFile = []byte(`<!doctype html>
 							<li class="nav-item">
 								<a class="nav-link{{if or (eq $.Data.Module "blog") (eq $.Data.Module "blog-post") (eq $.Data.Module "blog-category")}} active{{end}}" href="/blog/">Blog</a>
 							</li>
+							<li class="nav-item">
+								<a class="nav-link{{if or (eq $.Data.Module "shop") (eq $.Data.Module "shop-product") (eq $.Data.Module "shop-category")}} active{{end}}" href="/shop/">Shop</a>
+							</li>
 							<li class="nav-item">
 								<a class="nav-link{{if eq $.Data.Module "404"}} active{{end}}" href="/not-existent-page/">404</a>
 							</li>
@@ -86,6 +97,14 @@ var VarHeaderHtmlFile = []byte(`<!doctype html>
 									{{else}}
 										Blog
 									{{end}}
+								{{else if or (eq $.Data.Module "shop") (eq $.Data.Module "shop-product") (eq $.Data.Module "shop-category")}}
+									{{if eq $.Data.Module "shop-category"}}
+										Shop category
+									{{else if eq $.Data.Module "shop-product"}}
+										Shop product
+									{{else}}
+										Shop
+									{{end}}
 								{{end}}
 							{{else}}
 								Oops, page is not found...

+ 60 - 0
assets/template/shop_category_html_file.go

@@ -0,0 +1,60 @@
+package template
+
+var VarShopCategoryHtmlFile = []byte(`{{template "header.html" .}}
+<div class="card mb-4">
+	<div class="post">
+		<div class="card-body">
+			<b>Category author:</b> {{$.Data.Shop.Category.User.FirstName}} {{$.Data.Shop.Category.User.LastName}}
+		</div>
+	</div>
+</div>
+<div class="card mb-4">
+	{{if $.Data.Shop.HaveProducts}}
+		{{range $.Data.Shop.Products}}
+			<div class="post">
+				<div class="card-body">
+					<h2 class="card-title">
+						<a href="{{.Permalink}}">
+							{{.Name}}
+						</a>
+					</h2>
+					<div class="post-content">
+						{{.Briefly}}
+					</div>
+					<div class="post-date">
+						<div><small>Published on {{.DateTimeFormat "02/01/2006, 15:04:05"}}</small></div>
+						<div>Author: {{.User.FirstName}} {{.User.LastName}}</div>
+					</div>
+				</div>
+			</div>
+		{{end}}
+	{{else}}
+		<div class="card-body">
+			Sorry, no posts matched your criteria
+		</div>
+	{{end}}
+</div>
+{{if $.Data.Shop.HaveProducts}}
+	{{if gt $.Data.Shop.ProductsMaxPage 1 }}
+		<nav>
+			<ul class="pagination mb-4">
+				{{if $.Data.Shop.PaginationPrev}}
+					<li class="page-item{{if $.Data.Shop.PaginationPrev.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationPrev.Link}}">Previous</a>
+					</li>
+				{{end}}
+				{{range $.Data.Shop.Pagination}}
+					<li class="page-item{{if .Current}} active{{end}}">
+						<a class="page-link" href="{{.Link}}">{{.Num}}</a>
+					</li>
+				{{end}}
+				{{if $.Data.Shop.PaginationNext}}
+					<li class="page-item{{if $.Data.Shop.PaginationNext.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationNext.Link}}">Next</a>
+					</li>
+				{{end}}
+			</ul>
+		</nav>
+	{{end}}
+{{end}}
+{{template "footer.html" .}}`)

+ 57 - 0
assets/template/shop_html_file.go

@@ -0,0 +1,57 @@
+package template
+
+var VarShopHtmlFile = []byte(`{{template "header.html" .}}
+<div class="card mb-4">
+	{{if $.Data.Shop.HaveProducts}}
+		{{range $.Data.Shop.Products}}
+			<div class="post">
+				<div class="card-body">
+					<h2 class="card-title">
+						<a href="{{.Permalink}}">
+							{{.Name}}
+						</a>
+					</h2>
+					<div class="post-content">
+						{{.Briefly}}
+					</div>
+					<div class="post-date">
+						<div><small>Published on {{.DateTimeFormat "02/01/2006, 15:04:05"}}</small></div>
+						<div>Author: {{.User.FirstName}} {{.User.LastName}}</div>
+					</div>
+				</div>
+			</div>
+		{{end}}
+	{{else}}
+		<div class="card-body">
+			Sorry, no posts matched your criteria
+		</div>
+	{{end}}
+</div>
+{{if $.Data.Shop.HaveProducts}}
+	{{if gt $.Data.Shop.ProductsMaxPage 1 }}
+		<nav>
+			<ul class="pagination mb-4">
+				{{if $.Data.Shop.PaginationPrev}}
+					<li class="page-item{{if $.Data.Shop.PaginationPrev.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationPrev.Link}}">Previous</a>
+					</li>
+				{{end}}
+				{{range $.Data.Shop.Pagination}}
+					{{if .Dots}}
+						<li class="page-item disabled"><a class="page-link" href="">...</a></li>
+					{{else}}
+						<li class="page-item{{if .Current}} active{{end}}">
+							<a class="page-link" href="{{.Link}}">{{.Num}}</a>
+						</li>
+					{{end}}
+				{{end}}
+				{{if $.Data.Shop.PaginationNext}}
+					<li class="page-item{{if $.Data.Shop.PaginationNext.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationNext.Link}}">Next</a>
+					</li>
+				{{end}}
+			</ul>
+		</nav>
+	{{end}}
+{{end}}
+{{template "footer.html" .}}`)

+ 18 - 0
assets/template/shop_product_html_file.go

@@ -0,0 +1,18 @@
+package template
+
+var VarShopProductHtmlFile = []byte(`{{template "header.html" .}}
+<div class="card mb-4">
+	<div class="card-body">
+		<h2 class="card-title">{{$.Data.Shop.Product.Name}}</h2>
+		<div class="page-content">
+			{{$.Data.Shop.Product.Briefly}}
+			{{$.Data.Shop.Product.Content}}
+		</div>
+	</div>
+	<div class="card-footer text-muted">
+		<div>Price: {{$.Data.Shop.Product.PriceFormat "%.2f"}}</div>
+		<div>Published on {{$.Data.Shop.Product.DateTimeFormat "02/01/2006, 15:04:05"}}</div>
+		<div>Author: {{$.Data.Shop.Product.User.FirstName}} {{$.Data.Shop.Product.User.LastName}}</div>
+	</div>
+</div>
+{{template "footer.html" .}}`)

+ 14 - 2
assets/template/sidebar_right_html_file.go

@@ -1,7 +1,7 @@
 package template
 
 var VarSidebarRightHtmlFile = []byte(`<div class="card mb-4">
-	<h5 class="card-header">Categories</h5>
+	<h5 class="card-header">Blog Categories</h5>
 	<div class="card-body">
 		<ul class="m-0 p-0 pl-4">
 			{{range $.Data.Blog.Categories 0}}
@@ -13,7 +13,19 @@ var VarSidebarRightHtmlFile = []byte(`<div class="card mb-4">
 	</div>
 </div>
 <div class="card mb-4">
-	<h5 class="card-header">Useful links</h5>
+	<h5 class="card-header">Shop Categories</h5>
+	<div class="card-body">
+		<ul class="m-0 p-0 pl-4">
+			{{range $.Data.Shop.Categories 0}}
+				<li class="{{if and $.Data.Shop.Category (eq $.Data.Shop.Category.Id .Id)}}active{{end}}">
+					<a href="{{.Permalink}}">{{.Name}}</a>
+				</li>
+			{{end}}
+		</ul>
+	</div>
+</div>
+<div class="card mb-4">
+	<h5 class="card-header">Useful Links</h5>
 	<div class="card-body">
 		<ul class="m-0 p-0 pl-4">
 			<li><a href="https://github.com/vladimirok5959/golang-fave" target="_blank">Project on GitHub</a></li>

+ 3 - 0
assets/template/template.go

@@ -6,10 +6,13 @@ var AllData = map[string][]byte{
 	"styles.css":         VarStylesCssFile,
 	"header.html":        VarHeaderHtmlFile,
 	"blog.html":          VarBlogHtmlFile,
+	"shop-product.html":  VarShopProductHtmlFile,
 	"index.html":         VarIndexHtmlFile,
 	"robots.txt":         VarRobotsTxtFile,
 	"page.html":          VarPageHtmlFile,
 	"404.html":           Var404HtmlFile,
+	"shop.html":          VarShopHtmlFile,
+	"shop-category.html": VarShopCategoryHtmlFile,
 	"blog-post.html":     VarBlogPostHtmlFile,
 	"scripts.js":         VarScriptsJsFile,
 	"sidebar-left.html":  VarSidebarLeftHtmlFile,

+ 78 - 0
engine/fetdata/currency.go

@@ -0,0 +1,78 @@
+package fetdata
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+type Currency struct {
+	wrap   *wrapper.Wrapper
+	object *utils.MySql_shop_currency
+}
+
+func (this *Currency) load(id int) {
+	if this == nil {
+		return
+	}
+	if this.object != nil {
+		return
+	}
+	this.object = &utils.MySql_shop_currency{}
+	if err := this.wrap.DB.QueryRow(`
+		SELECT
+			id,
+			name,
+			coefficient,
+			code,
+			symbol
+		FROM
+			shop_currencies
+		WHERE
+			id = ?
+		LIMIT 1;`,
+		id,
+	).Scan(
+		&this.object.A_id,
+		&this.object.A_name,
+		&this.object.A_coefficient,
+		&this.object.A_code,
+		&this.object.A_symbol,
+	); err != nil {
+		return
+	}
+}
+
+func (this *Currency) Id() int {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_id
+}
+
+func (this *Currency) Name() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_name
+}
+
+func (this *Currency) Coefficient() float64 {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_coefficient
+}
+
+func (this *Currency) Code() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_code
+}
+
+func (this *Currency) Symbol() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_symbol
+}

+ 29 - 0
engine/fetdata/fetdata.go

@@ -13,11 +13,13 @@ type FERData struct {
 
 	Page *Page
 	Blog *Blog
+	Shop *Shop
 }
 
 func New(wrap *wrapper.Wrapper, drow interface{}, is404 bool) *FERData {
 	var d_Page *Page
 	var d_Blog *Blog
+	var d_Shop *Shop
 
 	if wrap.CurrModule == "index" {
 		if o, ok := drow.(*utils.MySql_page); ok {
@@ -37,17 +39,36 @@ func New(wrap *wrapper.Wrapper, drow interface{}, is404 bool) *FERData {
 			d_Blog = &Blog{wrap: wrap}
 			d_Blog.load()
 		}
+	} else if wrap.CurrModule == "shop" {
+		if len(wrap.UrlArgs) == 3 && wrap.UrlArgs[0] == "shop" && wrap.UrlArgs[1] == "category" && wrap.UrlArgs[2] != "" {
+			if o, ok := drow.(*utils.MySql_shop_category); ok {
+				d_Shop = &Shop{wrap: wrap, category: &ShopCategory{wrap: wrap, object: o}}
+				d_Shop.load()
+			}
+		} else if len(wrap.UrlArgs) == 2 && wrap.UrlArgs[0] == "shop" && wrap.UrlArgs[1] != "" {
+			if o, ok := drow.(*utils.MySql_shop_product); ok {
+				d_Shop = &Shop{wrap: wrap, product: &ShopProduct{wrap: wrap, object: o}}
+			}
+		} else {
+			d_Shop = &Shop{wrap: wrap}
+			d_Shop.load()
+		}
 	}
 
 	if d_Blog == nil {
 		d_Blog = &Blog{wrap: wrap}
 	}
 
+	if d_Shop == nil {
+		d_Shop = &Shop{wrap: wrap}
+	}
+
 	fer := &FERData{
 		wrap:  wrap,
 		is404: is404,
 		Page:  d_Page,
 		Blog:  d_Blog,
+		Shop:  d_Shop,
 	}
 
 	return fer
@@ -80,6 +101,14 @@ func (this *FERData) Module() string {
 		} else {
 			mod = "blog"
 		}
+	} else if this.wrap.CurrModule == "shop" {
+		if len(this.wrap.UrlArgs) == 3 && this.wrap.UrlArgs[0] == "shop" && this.wrap.UrlArgs[1] == "category" && this.wrap.UrlArgs[2] != "" {
+			mod = "shop-category"
+		} else if len(this.wrap.UrlArgs) == 2 && this.wrap.UrlArgs[0] == "shop" && this.wrap.UrlArgs[1] != "" {
+			mod = "shop-product"
+		} else {
+			mod = "shop"
+		}
 	}
 	return mod
 }

+ 440 - 0
engine/fetdata/shop.go

@@ -0,0 +1,440 @@
+package fetdata
+
+import (
+	"math"
+	"strings"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+type ShopPagination struct {
+	Num     string
+	Link    string
+	Current bool
+	Dots    bool
+}
+
+type Shop struct {
+	wrap     *wrapper.Wrapper
+	category *ShopCategory
+	product  *ShopProduct
+
+	products         []*ShopProduct
+	productsCount    int
+	productsPerPage  int
+	productsMaxPage  int
+	productsCurrPage int
+	pagination       []*ShopPagination
+	paginationPrev   *ShopPagination
+	paginationNext   *ShopPagination
+
+	bufferCats map[string][]*ShopCategory
+}
+
+func (this *Shop) load() {
+	if this == nil {
+		return
+	}
+	sql_nums := `
+		SELECT
+			COUNT(*)
+		FROM
+			shop_products
+		WHERE
+			active = 1
+		;
+	`
+	sql_rows := `
+		SELECT
+			shop_products.id,
+			shop_products.user,
+			shop_products.currency,
+			shop_products.price,
+			shop_products.name,
+			shop_products.alias,
+			shop_products.briefly,
+			shop_products.content,
+			UNIX_TIMESTAMP(shop_products.datetime) as datetime,
+			shop_products.active,
+			users.id,
+			users.first_name,
+			users.last_name,
+			users.email,
+			users.admin,
+			users.active,
+			shop_currencies.id,
+			shop_currencies.name,
+			shop_currencies.coefficient,
+			shop_currencies.code,
+			shop_currencies.symbol
+		FROM
+			shop_products
+			LEFT JOIN users ON users.id = shop_products.user
+			LEFT JOIN shop_currencies ON shop_currencies.id = shop_products.currency
+		WHERE
+			shop_products.active = 1
+		ORDER BY
+			shop_products.id DESC
+		LIMIT ?, ?;
+	`
+
+	// Category selected
+	if this.category != nil {
+		var cat_ids []string
+		if rows, err := this.wrap.DB.Query(
+			`SELECT
+				node.id
+			FROM
+				shop_cats AS node,
+				shop_cats AS parent
+			WHERE
+				node.lft BETWEEN parent.lft AND parent.rgt AND
+				node.id > 1 AND
+				parent.id = ?
+			GROUP BY
+				node.id
+			ORDER BY
+				node.lft ASC
+			;`,
+			this.category.Id(),
+		); err == nil {
+			defer rows.Close()
+			for rows.Next() {
+				var cat_id string
+				if err := rows.Scan(&cat_id); err == nil {
+					cat_ids = append(cat_ids, cat_id)
+				}
+			}
+		}
+		sql_nums = `
+			SELECT
+				COUNT(*)
+			FROM
+				(
+					SELECT
+						COUNT(*)
+					FROM
+						shop_products
+						LEFT JOIN shop_cat_product_rel ON shop_cat_product_rel.product_id = shop_products.id
+					WHERE
+						shop_products.active = 1 AND
+						shop_cat_product_rel.category_id IN (` + strings.Join(cat_ids, ", ") + `)
+					GROUP BY
+						shop_products.id
+				) AS tbl
+			;
+		`
+		sql_rows = `
+			SELECT
+				shop_products.id,
+				shop_products.user,
+				shop_products.currency,
+				shop_products.price,
+				shop_products.name,
+				shop_products.alias,
+				shop_products.briefly,
+				shop_products.content,
+				UNIX_TIMESTAMP(shop_products.datetime) AS datetime,
+				shop_products.active,
+				users.id,
+				users.first_name,
+				users.last_name,
+				users.email,
+				users.admin,
+				users.active,
+				shop_currencies.id,
+				shop_currencies.name,
+				shop_currencies.coefficient,
+				shop_currencies.code,
+				shop_currencies.symbol
+			FROM
+				shop_products
+				LEFT JOIN shop_cat_product_rel ON shop_cat_product_rel.product_id = shop_products.id
+				LEFT JOIN users ON users.id = shop_products.user
+				LEFT JOIN shop_currencies ON shop_currencies.id = shop_products.currency
+			WHERE
+				shop_products.active = 1 AND
+				shop_cat_product_rel.category_id IN (` + strings.Join(cat_ids, ", ") + `)
+			GROUP BY
+				shop_products.id
+			ORDER BY
+				shop_products.id DESC
+			LIMIT ?, ?;
+		`
+	}
+
+	if err := this.wrap.DB.QueryRow(sql_nums).Scan(&this.productsCount); err == nil {
+		if this.category == nil {
+			this.productsPerPage = (*this.wrap.Config).Shop.Pagination.Index
+		} else {
+			this.productsPerPage = (*this.wrap.Config).Shop.Pagination.Category
+		}
+		this.productsMaxPage = int(math.Ceil(float64(this.productsCount) / float64(this.productsPerPage)))
+		this.productsCurrPage = this.wrap.GetCurrentPage(this.productsMaxPage)
+		offset := this.productsCurrPage*this.productsPerPage - this.productsPerPage
+		if rows, err := this.wrap.DB.Query(sql_rows, offset, this.productsPerPage); err == nil {
+			defer rows.Close()
+			for rows.Next() {
+				rp := utils.MySql_shop_product{}
+				ru := utils.MySql_user{}
+				rc := utils.MySql_shop_currency{}
+				if err := rows.Scan(
+					&rp.A_id,
+					&rp.A_user,
+					&rp.A_currency,
+					&rp.A_price,
+					&rp.A_name,
+					&rp.A_alias,
+					&rp.A_briefly,
+					&rp.A_content,
+					&rp.A_datetime,
+					&rp.A_active,
+					&ru.A_id,
+					&ru.A_first_name,
+					&ru.A_last_name,
+					&ru.A_email,
+					&ru.A_admin,
+					&ru.A_active,
+					&rc.A_id,
+					&rc.A_name,
+					&rc.A_coefficient,
+					&rc.A_code,
+					&rc.A_symbol,
+				); err == nil {
+					this.products = append(this.products, &ShopProduct{
+						wrap:     this.wrap,
+						object:   &rp,
+						user:     &User{wrap: this.wrap, object: &ru},
+						currency: &Currency{wrap: this.wrap, object: &rc},
+					})
+				}
+			}
+		}
+	}
+
+	// Build pagination
+	if true {
+		for i := 1; i < this.productsCurrPage; i++ {
+			if this.productsCurrPage >= 5 && i > 1 && i < this.productsCurrPage-1 {
+				continue
+			}
+			if this.productsCurrPage >= 5 && i > 1 && i < this.productsCurrPage {
+				this.pagination = append(this.pagination, &ShopPagination{
+					Dots: true,
+				})
+			}
+			link := this.wrap.R.URL.Path
+			if i > 1 {
+				link = link + "?p=" + utils.IntToStr(i)
+			}
+			this.pagination = append(this.pagination, &ShopPagination{
+				Num:     utils.IntToStr(i),
+				Link:    link,
+				Current: false,
+			})
+		}
+
+		// Current page
+		link := this.wrap.R.URL.Path
+		if this.productsCurrPage > 1 {
+			link = link + "?p=" + utils.IntToStr(this.productsCurrPage)
+		}
+		this.pagination = append(this.pagination, &ShopPagination{
+			Num:     utils.IntToStr(this.productsCurrPage),
+			Link:    link,
+			Current: true,
+		})
+
+		for i := this.productsCurrPage + 1; i <= this.productsMaxPage; i++ {
+			if this.productsCurrPage < this.productsMaxPage-3 && i == this.productsCurrPage+3 {
+				this.pagination = append(this.pagination, &ShopPagination{
+					Dots: true,
+				})
+			}
+			if this.productsCurrPage < this.productsMaxPage-3 && i > this.productsCurrPage+1 && i <= this.productsMaxPage-1 {
+				continue
+			}
+			link := this.wrap.R.URL.Path
+			if i > 1 {
+				link = link + "?p=" + utils.IntToStr(i)
+			}
+			this.pagination = append(this.pagination, &ShopPagination{
+				Num:     utils.IntToStr(i),
+				Link:    link,
+				Current: false,
+			})
+		}
+	} else {
+		for i := 1; i <= this.productsMaxPage; i++ {
+			link := this.wrap.R.URL.Path
+			if i > 1 {
+				link = link + "?p=" + utils.IntToStr(i)
+			}
+			this.pagination = append(this.pagination, &ShopPagination{
+				Num:     utils.IntToStr(i),
+				Link:    link,
+				Current: i == this.productsCurrPage,
+			})
+		}
+	}
+
+	// Pagination prev/next
+	if this.productsMaxPage > 1 {
+		link := this.wrap.R.URL.Path
+		if this.productsCurrPage-1 > 1 {
+			link = this.wrap.R.URL.Path + "?p=" + utils.IntToStr(this.productsCurrPage-1)
+		}
+		this.paginationPrev = &ShopPagination{
+			Num:     utils.IntToStr(this.productsCurrPage - 1),
+			Link:    link,
+			Current: this.productsCurrPage <= 1,
+		}
+		if this.productsCurrPage >= 1 && this.productsCurrPage < this.productsMaxPage {
+			link = this.wrap.R.URL.Path + "?p=" + utils.IntToStr(this.productsCurrPage+1)
+		} else {
+			link = this.wrap.R.URL.Path + "?p=" + utils.IntToStr(this.productsMaxPage)
+		}
+		this.paginationNext = &ShopPagination{
+			Num:     utils.IntToStr(this.productsCurrPage + 1),
+			Link:    link,
+			Current: this.productsCurrPage >= this.productsMaxPage,
+		}
+	}
+}
+
+func (this *Shop) Category() *ShopCategory {
+	if this == nil {
+		return nil
+	}
+	return this.category
+}
+
+func (this *Shop) Product() *ShopProduct {
+	if this == nil {
+		return nil
+	}
+	return this.product
+}
+
+func (this *Shop) HaveProducts() bool {
+	if this == nil {
+		return false
+	}
+	if len(this.products) <= 0 {
+		return false
+	}
+	return true
+}
+
+func (this *Shop) Products() []*ShopProduct {
+	if this == nil {
+		return []*ShopProduct{}
+	}
+	return this.products
+}
+
+func (this *Shop) ProductsCount() int {
+	if this == nil {
+		return 0
+	}
+	return this.productsCount
+}
+
+func (this *Shop) ProductsPerPage() int {
+	if this == nil {
+		return 0
+	}
+	return this.productsPerPage
+}
+
+func (this *Shop) ProductsMaxPage() int {
+	if this == nil {
+		return 0
+	}
+	return this.productsMaxPage
+}
+
+func (this *Shop) ProductsCurrPage() int {
+	if this == nil {
+		return 0
+	}
+	return this.productsCurrPage
+}
+
+func (this *Shop) Pagination() []*ShopPagination {
+	if this == nil {
+		return []*ShopPagination{}
+	}
+	return this.pagination
+}
+
+func (this *Shop) PaginationPrev() *ShopPagination {
+	if this == nil {
+		return nil
+	}
+	return this.paginationPrev
+}
+
+func (this *Shop) PaginationNext() *ShopPagination {
+	if this == nil {
+		return nil
+	}
+	return this.paginationNext
+}
+
+func (this *Shop) Categories(mlvl int) []*ShopCategory {
+	if this == nil {
+		return []*ShopCategory{}
+	}
+	if this.bufferCats == nil {
+		this.bufferCats = map[string][]*ShopCategory{}
+	}
+	key := ""
+	where := ``
+	if mlvl > 0 {
+		where += `AND tbl.depth <= ` + utils.IntToStr(mlvl)
+	}
+	if _, ok := this.bufferCats[key]; !ok {
+		var cats []*ShopCategory
+		if rows, err := this.wrap.DB.Query(`
+			SELECT
+				tbl.*
+			FROM
+				(
+					SELECT
+						node.id,
+						node.user,
+						node.name,
+						node.alias,
+						node.lft,
+						node.rgt,
+						(COUNT(parent.id) - 1) AS depth
+					FROM
+						shop_cats AS node,
+						shop_cats AS parent
+					WHERE
+						node.lft BETWEEN parent.lft AND parent.rgt
+					GROUP BY
+						node.id
+					ORDER BY
+						node.lft ASC
+				) AS tbl
+			WHERE
+				tbl.id > 1
+				` + where + `
+			;
+		`); err == nil {
+			defer rows.Close()
+			for rows.Next() {
+				row := utils.MySql_shop_category{}
+				var Depth int
+				if err := rows.Scan(&row.A_id, &row.A_user, &row.A_name, &row.A_alias, &row.A_lft, &row.A_rgt, &Depth); err == nil {
+					cats = append(cats, &ShopCategory{object: &row, depth: Depth})
+				}
+			}
+		}
+		this.bufferCats[key] = cats
+	}
+	return this.bufferCats[key]
+}

+ 75 - 0
engine/fetdata/shop_category.go

@@ -0,0 +1,75 @@
+package fetdata
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+type ShopCategory struct {
+	wrap   *wrapper.Wrapper
+	object *utils.MySql_shop_category
+	depth  int
+
+	user *User
+}
+
+func (this *ShopCategory) Id() int {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_id
+}
+
+func (this *ShopCategory) User() *User {
+	if this == nil {
+		return nil
+	}
+	if this.user != nil {
+		return this.user
+	}
+	this.user = &User{wrap: this.wrap}
+	this.user.load(this.object.A_user)
+	return this.user
+}
+
+func (this *ShopCategory) Name() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_name
+}
+
+func (this *ShopCategory) Alias() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_alias
+}
+
+func (this *ShopCategory) Left() int {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_lft
+}
+
+func (this *ShopCategory) Right() int {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_rgt
+}
+
+func (this *ShopCategory) Permalink() string {
+	if this == nil {
+		return ""
+	}
+	return "/shop/category/" + this.object.A_alias + "/"
+}
+
+func (this *ShopCategory) Level() int {
+	if this == nil {
+		return 0
+	}
+	return this.depth
+}

+ 118 - 0
engine/fetdata/shop_product.go

@@ -0,0 +1,118 @@
+package fetdata
+
+import (
+	"html/template"
+	"time"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+type ShopProduct struct {
+	wrap   *wrapper.Wrapper
+	object *utils.MySql_shop_product
+
+	user     *User
+	currency *Currency
+}
+
+func (this *ShopProduct) Id() int {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_id
+}
+
+func (this *ShopProduct) User() *User {
+	if this == nil {
+		return nil
+	}
+	if this.user != nil {
+		return this.user
+	}
+	this.user = &User{wrap: this.wrap}
+	this.user.load(this.object.A_user)
+	return this.user
+}
+
+func (this *ShopProduct) Currency() *Currency {
+	if this == nil {
+		return nil
+	}
+	if this.currency != nil {
+		return this.currency
+	}
+	this.currency = &Currency{wrap: this.wrap}
+	this.currency.load(this.object.A_currency)
+	return this.currency
+}
+
+func (this *ShopProduct) Price() float64 {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_price
+}
+
+func (this *ShopProduct) PriceFormat(format string) string {
+	if this == nil {
+		return ""
+	}
+	return utils.Float64ToStrF(this.object.A_price, format)
+}
+
+func (this *ShopProduct) Name() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_name
+}
+
+func (this *ShopProduct) Alias() string {
+	if this == nil {
+		return ""
+	}
+	return this.object.A_alias
+}
+
+func (this *ShopProduct) Briefly() template.HTML {
+	if this == nil {
+		return template.HTML("")
+	}
+	return template.HTML(this.object.A_briefly)
+}
+
+func (this *ShopProduct) Content() template.HTML {
+	if this == nil {
+		return template.HTML("")
+	}
+	return template.HTML(this.object.A_content)
+}
+
+func (this *ShopProduct) DateTimeUnix() int {
+	if this == nil {
+		return 0
+	}
+	return this.object.A_datetime
+}
+
+func (this *ShopProduct) DateTimeFormat(format string) string {
+	if this == nil {
+		return ""
+	}
+	return time.Unix(int64(this.object.A_datetime), 0).Format(format)
+}
+
+func (this *ShopProduct) Active() bool {
+	if this == nil {
+		return false
+	}
+	return this.object.A_active > 0
+}
+
+func (this *ShopProduct) Permalink() string {
+	if this == nil {
+		return ""
+	}
+	return "/shop/" + this.object.A_alias + "/"
+}

+ 19 - 0
hosts/localhost/template/header.html

@@ -21,6 +21,14 @@
 					{{else}}
 						Latest posts | Blog
 					{{end}}
+				{{else if or (eq $.Data.Module "shop") (eq $.Data.Module "shop-product") (eq $.Data.Module "shop-category")}}
+					{{if eq $.Data.Module "shop-category"}}
+						Products of category "{{$.Data.Shop.Category.Name}}" | Shop
+					{{else if eq $.Data.Module "shop-product"}}
+						{{$.Data.Shop.Product.Name}} | Shop
+					{{else}}
+						Latest products | Shop
+					{{end}}
 				{{end}}
 			{{else}}
 				Error 404
@@ -58,6 +66,9 @@
 							<li class="nav-item">
 								<a class="nav-link{{if or (eq $.Data.Module "blog") (eq $.Data.Module "blog-post") (eq $.Data.Module "blog-category")}} active{{end}}" href="/blog/">Blog</a>
 							</li>
+							<li class="nav-item">
+								<a class="nav-link{{if or (eq $.Data.Module "shop") (eq $.Data.Module "shop-product") (eq $.Data.Module "shop-category")}} active{{end}}" href="/shop/">Shop</a>
+							</li>
 							<li class="nav-item">
 								<a class="nav-link{{if eq $.Data.Module "404"}} active{{end}}" href="/not-existent-page/">404</a>
 							</li>
@@ -84,6 +95,14 @@
 									{{else}}
 										Blog
 									{{end}}
+								{{else if or (eq $.Data.Module "shop") (eq $.Data.Module "shop-product") (eq $.Data.Module "shop-category")}}
+									{{if eq $.Data.Module "shop-category"}}
+										Shop category
+									{{else if eq $.Data.Module "shop-product"}}
+										Shop product
+									{{else}}
+										Shop
+									{{end}}
 								{{end}}
 							{{else}}
 								Oops, page is not found...

+ 59 - 0
hosts/localhost/template/shop-category.html

@@ -0,0 +1,59 @@
+{{template "header.html" .}}
+<div class="card mb-4">
+	<div class="post">
+		<div class="card-body">
+			<b>Category:</b> {{$.Data.Shop.Category.Name}}
+		</div>
+	</div>
+</div>
+<div class="card mb-4">
+	{{if $.Data.Shop.HaveProducts}}
+		{{range $.Data.Shop.Products}}
+			<div class="post">
+				<div class="card-body">
+					<h2 class="card-title">
+						<a href="{{.Permalink}}">
+							{{.Name}}
+						</a>
+					</h2>
+					<div class="post-content">
+						{{.Briefly}}
+					</div>
+					<div class="post-date">
+						<div><small>Price: {{.PriceFormat "%.2f"}} {{.Currency.Code}}</small></div>
+						<div><small>Published on {{.DateTimeFormat "02/01/2006, 15:04:05"}}</small></div>
+						<div>Author: {{.User.FirstName}} {{.User.LastName}}</div>
+					</div>
+				</div>
+			</div>
+		{{end}}
+	{{else}}
+		<div class="card-body">
+			Sorry, no products matched your criteria
+		</div>
+	{{end}}
+</div>
+{{if $.Data.Shop.HaveProducts}}
+	{{if gt $.Data.Shop.ProductsMaxPage 1 }}
+		<nav>
+			<ul class="pagination mb-4">
+				{{if $.Data.Shop.PaginationPrev}}
+					<li class="page-item{{if $.Data.Shop.PaginationPrev.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationPrev.Link}}">Previous</a>
+					</li>
+				{{end}}
+				{{range $.Data.Shop.Pagination}}
+					<li class="page-item{{if .Current}} active{{end}}">
+						<a class="page-link" href="{{.Link}}">{{.Num}}</a>
+					</li>
+				{{end}}
+				{{if $.Data.Shop.PaginationNext}}
+					<li class="page-item{{if $.Data.Shop.PaginationNext.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationNext.Link}}">Next</a>
+					</li>
+				{{end}}
+			</ul>
+		</nav>
+	{{end}}
+{{end}}
+{{template "footer.html" .}}

+ 16 - 0
hosts/localhost/template/shop-product.html

@@ -0,0 +1,16 @@
+{{template "header.html" .}}
+<div class="card mb-4">
+	<div class="card-body">
+		<h2 class="card-title">{{$.Data.Shop.Product.Name}}</h2>
+		<div class="page-content">
+			{{$.Data.Shop.Product.Briefly}}
+			{{$.Data.Shop.Product.Content}}
+		</div>
+	</div>
+	<div class="card-footer text-muted">
+		<div>Price: {{$.Data.Shop.Product.PriceFormat "%.2f"}} {{$.Data.Shop.Product.Currency.Code}}</div>
+		<div>Published on {{$.Data.Shop.Product.DateTimeFormat "02/01/2006, 15:04:05"}}</div>
+		<div>Author: {{$.Data.Shop.Product.User.FirstName}} {{$.Data.Shop.Product.User.LastName}}</div>
+	</div>
+</div>
+{{template "footer.html" .}}

+ 56 - 0
hosts/localhost/template/shop.html

@@ -0,0 +1,56 @@
+{{template "header.html" .}}
+<div class="card mb-4">
+	{{if $.Data.Shop.HaveProducts}}
+		{{range $.Data.Shop.Products}}
+			<div class="post">
+				<div class="card-body">
+					<h2 class="card-title">
+						<a href="{{.Permalink}}">
+							{{.Name}}
+						</a>
+					</h2>
+					<div class="post-content">
+						{{.Briefly}}
+					</div>
+					<div class="post-date">
+						<div><small>Price: {{.PriceFormat "%.2f"}} {{.Currency.Code}}</small></div>
+						<div><small>Published on {{.DateTimeFormat "02/01/2006, 15:04:05"}}</small></div>
+						<div>Author: {{.User.FirstName}} {{.User.LastName}}</div>
+					</div>
+				</div>
+			</div>
+		{{end}}
+	{{else}}
+		<div class="card-body">
+			Sorry, no products matched your criteria
+		</div>
+	{{end}}
+</div>
+{{if $.Data.Shop.HaveProducts}}
+	{{if gt $.Data.Shop.ProductsMaxPage 1 }}
+		<nav>
+			<ul class="pagination mb-4">
+				{{if $.Data.Shop.PaginationPrev}}
+					<li class="page-item{{if $.Data.Shop.PaginationPrev.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationPrev.Link}}">Previous</a>
+					</li>
+				{{end}}
+				{{range $.Data.Shop.Pagination}}
+					{{if .Dots}}
+						<li class="page-item disabled"><a class="page-link" href="">...</a></li>
+					{{else}}
+						<li class="page-item{{if .Current}} active{{end}}">
+							<a class="page-link" href="{{.Link}}">{{.Num}}</a>
+						</li>
+					{{end}}
+				{{end}}
+				{{if $.Data.Shop.PaginationNext}}
+					<li class="page-item{{if $.Data.Shop.PaginationNext.Current}} disabled{{end}}">
+						<a class="page-link" href="{{$.Data.Shop.PaginationNext.Link}}">Next</a>
+					</li>
+				{{end}}
+			</ul>
+		</nav>
+	{{end}}
+{{end}}
+{{template "footer.html" .}}

+ 14 - 2
hosts/localhost/template/sidebar-right.html

@@ -1,5 +1,5 @@
 <div class="card mb-4">
-	<h5 class="card-header">Categories</h5>
+	<h5 class="card-header">Blog Categories</h5>
 	<div class="card-body">
 		<ul class="m-0 p-0 pl-4">
 			{{range $.Data.Blog.Categories 0}}
@@ -11,7 +11,19 @@
 	</div>
 </div>
 <div class="card mb-4">
-	<h5 class="card-header">Useful links</h5>
+	<h5 class="card-header">Shop Categories</h5>
+	<div class="card-body">
+		<ul class="m-0 p-0 pl-4">
+			{{range $.Data.Shop.Categories 0}}
+				<li class="{{if and $.Data.Shop.Category (eq $.Data.Shop.Category.Id .Id)}}active{{end}}">
+					<a href="{{.Permalink}}">{{.Name}}</a>
+				</li>
+			{{end}}
+		</ul>
+	</div>
+</div>
+<div class="card mb-4">
+	<h5 class="card-header">Useful Links</h5>
 	<div class="card-body">
 		<ul class="m-0 p-0 pl-4">
 			<li><a href="https://github.com/vladimirok5959/golang-fave" target="_blank">Project on GitHub</a></li>

+ 0 - 0
utils/mysql_struct_shop_post.go → utils/mysql_struct_shop_product.go