Browse Source

CP shop module

Vova Tkach 5 years ago
parent
commit
de2d4bedaf
40 changed files with 3693 additions and 196 deletions
  1. 1 1
      Makefile
  2. 100 1
      assets/cp.scripts.js
  3. 0 0
      assets/cp.scripts.js.go
  4. 41 1
      assets/cp.styles.css
  5. 0 0
      assets/cp.styles.css.go
  6. 0 0
      assets/tmpl.cp.base.go
  7. 3 2
      assets/tmpl.cp.base.html
  8. 1 1
      consts/consts.go
  9. 16 2
      engine/builder/data_form.go
  10. 2 2
      engine/builder/data_table.go
  11. 9 0
      engine/wrapper/config.go
  12. 16 14
      modules/module_blog.go
  13. 1 1
      modules/module_blog_act_modify.go
  14. 11 7
      modules/module_index.go
  15. 7 0
      modules/module_index_act_cypress.go
  16. 444 91
      modules/module_index_act_mysql_setup.go
  17. 27 0
      modules/module_settings.go
  18. 31 0
      modules/module_settings_act_pagination.go
  19. 1245 0
      modules/module_shop.go
  20. 52 0
      modules/module_shop_act_delete.go
  21. 59 0
      modules/module_shop_act_get_attribute_values.go
  22. 271 0
      modules/module_shop_act_modify.go
  23. 82 0
      modules/module_shop_attributes_act_delete.go
  24. 201 0
      modules/module_shop_attributes_act_modify.go
  25. 91 0
      modules/module_shop_categories.go
  26. 60 0
      modules/module_shop_categories_act_delete.go
  27. 227 0
      modules/module_shop_categories_act_modify.go
  28. 40 0
      modules/module_shop_currencies_act_delete.go
  29. 104 0
      modules/module_shop_currencies_act_modify.go
  30. 1 1
      modules/modules.go
  31. 2 0
      support/migrate/000000001.go
  32. 10 0
      support/migrate/000000002.go
  33. 293 0
      support/migrate/000000003.go
  34. 143 72
      support/schema.sql
  35. 10 0
      utils/mysql_struct_shop_category.go
  36. 9 0
      utils/mysql_struct_shop_currency.go
  37. 7 0
      utils/mysql_struct_shop_filter.go
  38. 14 0
      utils/mysql_struct_shop_post.go
  39. 29 0
      utils/utils.go
  40. 33 0
      utils/utils_test.go

+ 1 - 1
Makefile

@@ -48,7 +48,7 @@ version:
 	@echo "const ServerVersion = \"${VERSION}\"" >> consts/consts_version.go
 	@echo "const ServerVersion = \"${VERSION}\"" >> consts/consts_version.go
 
 
 template:
 template:
-	./support/template.sh
+	@./support/template.sh
 	@gofmt -w ./assets/template/
 	@gofmt -w ./assets/template/
 
 
 dockerfile:
 dockerfile:

+ 100 - 1
assets/cp.scripts.js

@@ -3363,16 +3363,49 @@
 			}
 			}
 			if(modal_alert_place.length) {
 			if(modal_alert_place.length) {
 				modal_alert_place.html(GetModalAlertTmpl(title, message, error));
 				modal_alert_place.html(GetModalAlertTmpl(title, message, error));
+			} else {
+				ShowSystemMsgModal(title, message, error);
 			}
 			}
 		};
 		};
 
 
+		function ShowSystemMsgModal(title, message, error) {
+			$('#sys-modal-system-message-placeholder').html('');
+			var html = '<div class="modal fade" id="sys-modal-system-message" tabindex="-1" role="dialog" aria-labelledby="sysModalSystemMessageLabel" aria-hidden="true"> \
+				<div class="modal-dialog modal-dialog-centered" role="document"> \
+					<div class="modal-content"> \
+							<input type="hidden" name="action" value="index-user-update-profile"> \
+							<div class="modal-header"> \
+								<h5 class="modal-title" id="sysModalSystemMessageLabel">' + title + '</h5> \
+								<button type="button" class="close" data-dismiss="modal" aria-label="Close"> \
+									<span aria-hidden="true">&times;</span> \
+								</button> \
+							</div> \
+							<div class="modal-body text-left">' + message + '</div> \
+							<div class="modal-footer"> \
+								<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> \
+							</div> \
+					</div> \
+				</div> \
+			</div>';
+			$('#sys-modal-system-message-placeholder').html(html);
+			$('#sys-modal-system-message').modal({
+				backdrop: 'static',
+				keyboard: true,
+				show: false,
+			});
+			$('#sys-modal-system-message').on('hidden.bs.modal', function(e) {
+				$('#sys-modal-system-message-placeholder').html('');
+			});
+			$('#sys-modal-system-message').modal('show');
+		};
+
 		function AjaxEval(data) {
 		function AjaxEval(data) {
 			try {
 			try {
 				eval(data);
 				eval(data);
 			} catch(e) {
 			} catch(e) {
 				if(e instanceof SyntaxError) {
 				if(e instanceof SyntaxError) {
 					console.log(data);
 					console.log(data);
-					console.log('Error: JavaScript code eval error', e.message)
+					console.log('Error: JavaScript code eval error', e.message);
 				}
 				}
 			}
 			}
 		};
 		};
@@ -3464,6 +3497,12 @@
 			}
 			}
 		};
 		};
 
 
+		function PreventDataLost() {
+			if(!FormDataWasChanged) {
+				FormDataWasChanged = true;
+			}
+		};
+
 		function HtmlDecode(value) {
 		function HtmlDecode(value) {
 			var doc = new DOMParser().parseFromString(value, "text/html");
 			var doc = new DOMParser().parseFromString(value, "text/html");
 			return doc.documentElement.textContent;
 			return doc.documentElement.textContent;
@@ -3672,6 +3711,10 @@
 				ShowSystemMsg(title, message, true);
 				ShowSystemMsg(title, message, true);
 			},
 			},
 
 
+			FormDataWasChanged: function() {
+				PreventDataLost();
+			},
+
 			ModalUserProfile: function() {
 			ModalUserProfile: function() {
 				var html = '<div class="modal fade" id="sys-modal-user-settings" tabindex="-1" role="dialog" aria-labelledby="sysModalUserSettingsLabel" aria-hidden="true"> \
 				var html = '<div class="modal fade" id="sys-modal-user-settings" tabindex="-1" role="dialog" aria-labelledby="sysModalUserSettingsLabel" aria-hidden="true"> \
 					<div class="modal-dialog modal-dialog-centered" role="document"> \
 					<div class="modal-dialog modal-dialog-centered" role="document"> \
@@ -3725,6 +3768,62 @@
 				$("#sys-modal-user-settings").modal('show');
 				$("#sys-modal-user-settings").modal('show');
 			},
 			},
 
 
+			ShopProductsAdd: function() {
+				var selText = $('#lbl_attributes option:selected').text();
+				var selValue = $('#lbl_attributes').val();
+				if(selValue == '0') { return; }
+				$('#lbl_attributes')[0].selectedIndex = 0;
+				$('#lbl_attributes').selectpicker('refresh');
+				if($('#prod_attr_' + selValue).length > 0) { return; }
+				$('#list').append('<div class="form-group" id="prod_attr_' + selValue + '"><div><b>' + selText + '</b></div><div style="position:relative;"><select class="form-control" name="value.' + selValue + '" autocomplete="off" required disabled><option value="0">Loading values...</option></select><button type="button" class="btn btn-danger" style="position:absolute;top:0px;right:0px;" onclick="fave.ShopProductsRemove(this);" disabled>&times;</button></div></div>');
+				PreventDataLost();
+				$.ajax({
+					type: 'POST',
+					url: '/cp/',
+					data: {
+						action: 'shop-get-attribute-values',
+						id: selValue
+					}
+				}).done(function(data) {
+					try {
+						eval(data);
+					} catch(e) {
+						if(e instanceof SyntaxError) {
+							console.log(data);
+							console.log('Error: JavaScript code eval error', e.message);
+						}
+					}
+				}).fail(function(xhr, status, error) {
+					$('#prod_attr_' + selValue).remove();
+					try {
+						eval(xhr.responseText);
+					} catch(e) {
+						if(e instanceof SyntaxError) {
+							console.log(xhr.responseText);
+							console.log('Error: JavaScript code eval error', e.message);
+						}
+					}
+				});
+			},
+
+			ShopProductsRemove: function(button) {
+				$(button).parent().parent().remove();
+				PreventDataLost();
+			},
+
+			ShopAttributesAdd: function() {
+				$('#list').append('<div class="form-group" style="position:relative;"><input class="form-control" type="text" name="value.0" value="" placeholder="" autocomplete="off" required><button type="button" class="btn btn-danger" style="position:absolute;top:0px;right:0px;" onclick="fave.ShopAttributesRemove(this);">&times;</button></div>');
+				PreventDataLost();
+				setTimeout(function() {
+					$('#list input').last().focus();
+				}, 100);
+			},
+
+			ShopAttributesRemove: function(button) {
+				$(button).parent().remove();
+				PreventDataLost();
+			},
+
 			ActionLogout: function(message) {
 			ActionLogout: function(message) {
 				if(confirm(message)) {
 				if(confirm(message)) {
 					$.ajax({
 					$.ajax({

File diff suppressed because it is too large
+ 0 - 0
assets/cp.scripts.js.go


+ 41 - 1
assets/cp.styles.css

@@ -791,7 +791,7 @@ ul.pagination {
 	text-align: right;
 	text-align: right;
 }
 }
 
 
-/* Admin table: blog_posts */
+/* Admin table: table_blog_posts */
 .data-table.table_blog_posts .col_datetime {
 .data-table.table_blog_posts .col_datetime {
 	width: 8rem;
 	width: 8rem;
 }
 }
@@ -811,6 +811,46 @@ ul.pagination {
 	text-align: right;
 	text-align: right;
 }
 }
 
 
+/* Admin table: table_shop_products */
+.data-table.table_shop_products .col_price {
+	width: 8rem;
+}
+
+.data-table.table_shop_products .col_datetime {
+	width: 8rem;
+}
+
+.data-table.table_shop_products .col_active {
+	width: 5rem;
+}
+
+.data-table.table_shop_products .col_action {
+	width: 6rem;
+	text-align: right;
+}
+
+/* Admin table: table_shop_cats */
+.data-table.table_shop_cats .col_action {
+	width: 6rem;
+	text-align: right;
+}
+
+/* Admin table: table_shop_filters */
+.data-table.table_shop_filters .col_action {
+	width: 6rem;
+	text-align: right;
+}
+
+/* Admin table: table_shop_currencies */
+.data-table.table_shop_currencies .col_coefficient {
+	width: 7rem;
+}
+
+.data-table.table_shop_currencies .col_action {
+	width: 6rem;
+	text-align: right;
+}
+
 /* Admin table: table_users */
 /* Admin table: table_users */
 .data-table.table_users .col_active,
 .data-table.table_users .col_active,
 .data-table.table_users .col_admin {
 .data-table.table_users .col_admin {

File diff suppressed because it is too large
+ 0 - 0
assets/cp.styles.css.go


File diff suppressed because it is too large
+ 0 - 0
assets/tmpl.cp.base.go


+ 3 - 2
assets/tmpl.cp.base.html

@@ -6,7 +6,7 @@
 		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 		<title>{{$.Data.Title}}</title>
 		<title>{{$.Data.Title}}</title>
 		<link rel="stylesheet" href="{{$.System.PathCssBootstrap}}">
 		<link rel="stylesheet" href="{{$.System.PathCssBootstrap}}">
-		{{if or (eq $.System.CpModule "index") (eq $.System.CpModule "blog")}}
+		{{if or (eq $.System.CpModule "index") (eq $.System.CpModule "blog") (eq $.System.CpModule "shop")}}
 			{{if or (eq $.System.CpSubModule "add") (eq $.System.CpSubModule "modify")}}
 			{{if or (eq $.System.CpSubModule "add") (eq $.System.CpSubModule "modify")}}
 				<link rel="stylesheet" href="{{$.System.PathCssCpWysiwygPell}}">
 				<link rel="stylesheet" href="{{$.System.PathCssCpWysiwygPell}}">
 			{{end}}
 			{{end}}
@@ -26,6 +26,7 @@
 		</script>
 		</script>
 	</head>
 	</head>
 	<body class="{{$.Data.BodyClasses}} cp-mod-{{$.System.CpModule}} cp-sub-mod-{{$.System.CpSubModule}}">
 	<body class="{{$.Data.BodyClasses}} cp-mod-{{$.System.CpModule}} cp-sub-mod-{{$.System.CpSubModule}}">
+		<div id="sys-modal-system-message-placeholder"></div>
 		<div id="sys-modal-user-settings-placeholder"></div>
 		<div id="sys-modal-user-settings-placeholder"></div>
 		<nav class="navbar main navbar-expand-md navbar-dark fixed-top bg-dark">
 		<nav class="navbar main navbar-expand-md navbar-dark fixed-top bg-dark">
 			<a class="navbar-brand" href="/cp/">{{$.Data.Caption}}</a>
 			<a class="navbar-brand" href="/cp/">{{$.Data.Caption}}</a>
@@ -91,7 +92,7 @@
 		<script src="{{$.System.PathJsJquery}}"></script>
 		<script src="{{$.System.PathJsJquery}}"></script>
 		<script src="{{$.System.PathJsPopper}}"></script>
 		<script src="{{$.System.PathJsPopper}}"></script>
 		<script src="{{$.System.PathJsBootstrap}}"></script>
 		<script src="{{$.System.PathJsBootstrap}}"></script>
-		{{if or (eq $.System.CpModule "index") (eq $.System.CpModule "blog")}}
+		{{if or (eq $.System.CpModule "index") (eq $.System.CpModule "blog") (eq $.System.CpModule "shop")}}
 			{{if or (eq $.System.CpSubModule "add") (eq $.System.CpSubModule "modify")}}
 			{{if or (eq $.System.CpSubModule "add") (eq $.System.CpSubModule "modify")}}
 				<script src="{{$.System.PathJsCpWysiwygPell}}"></script>
 				<script src="{{$.System.PathJsCpWysiwygPell}}"></script>
 			{{end}}
 			{{end}}

+ 1 - 1
consts/consts.go

@@ -5,7 +5,7 @@ import (
 )
 )
 
 
 const AssetsPath = "assets"
 const AssetsPath = "assets"
-const AssetsVersion = "33"
+const AssetsVersion = "35"
 const DirIndexFile = "index.html"
 const DirIndexFile = "index.html"
 
 
 // Bootstrap resources
 // Bootstrap resources

+ 16 - 2
engine/builder/data_form.go

@@ -75,7 +75,14 @@ func DataForm(wrap *wrapper.Wrapper, data []DataFormField) string {
 				html_element += `<div class="col-md-9">`
 				html_element += `<div class="col-md-9">`
 				html_element += `<div>`
 				html_element += `<div>`
 				if field.Kind == DFKText {
 				if field.Kind == DFKText {
-					html_element += `<input class="form-control` + classes + `" type="text" id="lbl_` + field.Name + `" name="` + field.Name + `" value="` + html.EscapeString(field.Value) + `" placeholder="` + field.Placeholder + `" autocomplete="off"` + required + `>`
+					html_element += `<input class="form-control` + classes + `" type="text" id="lbl_` + field.Name + `" name="` + field.Name + `" value="` + html.EscapeString(field.Value) + `" `
+					if field.Min != "" {
+						html_element += `minlength="` + field.Min + `" `
+					}
+					if field.Max != "" {
+						html_element += `maxlength="` + field.Max + `" `
+					}
+					html_element += `placeholder="` + field.Placeholder + `" autocomplete="off"` + required + `>`
 				} else if field.Kind == DFKNumber {
 				} else if field.Kind == DFKNumber {
 					html_element += `<input class="form-control` + classes + `" type="number" id="lbl_` + field.Name + `" name="` + field.Name + `" value="` + html.EscapeString(field.Value) + `" `
 					html_element += `<input class="form-control` + classes + `" type="number" id="lbl_` + field.Name + `" name="` + field.Name + `" value="` + html.EscapeString(field.Value) + `" `
 					if field.Min != "" {
 					if field.Min != "" {
@@ -90,7 +97,14 @@ func DataForm(wrap *wrapper.Wrapper, data []DataFormField) string {
 				} else if field.Kind == DFKPassword {
 				} else if field.Kind == DFKPassword {
 					html_element += `<input class="form-control` + classes + `" type="password" id="lbl_` + field.Name + `" name="` + field.Name + `" value="` + html.EscapeString(field.Value) + `" placeholder="` + field.Placeholder + `" autocomplete="off"` + required + `>`
 					html_element += `<input class="form-control` + classes + `" type="password" id="lbl_` + field.Name + `" name="` + field.Name + `" value="` + html.EscapeString(field.Value) + `" placeholder="` + field.Placeholder + `" autocomplete="off"` + required + `>`
 				} else if field.Kind == DFKTextArea {
 				} else if field.Kind == DFKTextArea {
-					html_element += `<textarea class="form-control` + classes + `" id="lbl_` + field.Name + `" name="` + field.Name + `" placeholder="` + field.Placeholder + `" autocomplete="off"` + required + `>` + html.EscapeString(field.Value) + `</textarea>`
+					html_element += `<textarea class="form-control` + classes + `" id="lbl_` + field.Name + `" name="` + field.Name + `" `
+					if field.Min != "" {
+						html_element += `minlength="` + field.Min + `" `
+					}
+					if field.Max != "" {
+						html_element += `maxlength="` + field.Max + `" `
+					}
+					html_element += `placeholder="` + field.Placeholder + `" autocomplete="off"` + required + `>` + html.EscapeString(field.Value) + `</textarea>`
 				} else if field.Kind == DFKCheckBox {
 				} else if field.Kind == DFKCheckBox {
 					checked := ""
 					checked := ""
 					if field.Value != "0" {
 					if field.Value != "0" {

+ 2 - 2
engine/builder/data_table.go

@@ -144,10 +144,10 @@ func DataTable(
 			rows.Close()
 			rows.Close()
 		}
 		}
 		if !have_records {
 		if !have_records {
-			result += `<tr><td colspan="50">No any data found</td></tr>`
+			result += `<tr><td colspan="50">No data</td></tr>`
 		}
 		}
 	} else {
 	} else {
-		result += `<tr><td colspan="50">No any data found</td></tr>`
+		result += `<tr><td colspan="50">No data</td></tr>`
 	}
 	}
 	result += `</tbody></table>`
 	result += `</tbody></table>`
 
 

+ 9 - 0
engine/wrapper/config.go

@@ -12,6 +12,12 @@ type Config struct {
 			Category int
 			Category int
 		}
 		}
 	}
 	}
+	Shop struct {
+		Pagination struct {
+			Index    int
+			Category int
+		}
+	}
 }
 }
 
 
 func configNew() *Config {
 func configNew() *Config {
@@ -23,6 +29,9 @@ func configNew() *Config {
 func (this *Config) configDefault() {
 func (this *Config) configDefault() {
 	this.Blog.Pagination.Index = 5
 	this.Blog.Pagination.Index = 5
 	this.Blog.Pagination.Category = 5
 	this.Blog.Pagination.Category = 5
+
+	this.Shop.Pagination.Index = 5
+	this.Shop.Pagination.Category = 5
 }
 }
 
 
 func (this *Config) configRead(file string) error {
 func (this *Config) configRead(file string) error {

+ 16 - 14
modules/module_blog.go

@@ -397,10 +397,13 @@ func (this *Modules) RegisterModule_Blog() *Module {
 					Value: utils.IntToStr(data.A_id),
 					Value: utils.IntToStr(data.A_id),
 				},
 				},
 				{
 				{
-					Kind:    builder.DFKText,
-					Caption: "Post name",
-					Name:    "name",
-					Value:   data.A_name,
+					Kind:     builder.DFKText,
+					Caption:  "Post name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKText,
 					Kind:    builder.DFKText,
@@ -408,6 +411,7 @@ func (this *Modules) RegisterModule_Blog() *Module {
 					Name:    "alias",
 					Name:    "alias",
 					Value:   data.A_alias,
 					Value:   data.A_alias,
 					Hint:    "Example: our-news",
 					Hint:    "Example: our-news",
+					Max:     "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKText,
 					Kind:    builder.DFKText,
@@ -451,9 +455,6 @@ func (this *Modules) RegisterModule_Blog() *Module {
 					Name:    "active",
 					Name:    "active",
 					Value:   utils.IntToStr(data.A_active),
 					Value:   utils.IntToStr(data.A_active),
 				},
 				},
-				{
-					Kind: builder.DFKMessage,
-				},
 				{
 				{
 					Kind:   builder.DFKSubmit,
 					Kind:   builder.DFKSubmit,
 					Value:  btn_caption,
 					Value:  btn_caption,
@@ -567,10 +568,13 @@ func (this *Modules) RegisterModule_Blog() *Module {
 					},
 					},
 				},
 				},
 				{
 				{
-					Kind:    builder.DFKText,
-					Caption: "Name",
-					Name:    "name",
-					Value:   data.A_name,
+					Kind:     builder.DFKText,
+					Caption:  "Name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKText,
 					Kind:    builder.DFKText,
@@ -578,9 +582,7 @@ func (this *Modules) RegisterModule_Blog() *Module {
 					Name:    "alias",
 					Name:    "alias",
 					Value:   data.A_alias,
 					Value:   data.A_alias,
 					Hint:    "Example: popular-posts",
 					Hint:    "Example: popular-posts",
-				},
-				{
-					Kind: builder.DFKMessage,
+					Max:     "255",
 				},
 				},
 				{
 				{
 					Kind:   builder.DFKSubmit,
 					Kind:   builder.DFKSubmit,

+ 1 - 1
modules/module_blog_act_modify.go

@@ -31,7 +31,7 @@ func (this *Modules) RegisterAction_BlogModify() *Action {
 		}
 		}
 
 
 		if pf_name == "" {
 		if pf_name == "" {
-			wrap.MsgError(`Please specify page name`)
+			wrap.MsgError(`Please specify post name`)
 			return
 			return
 		}
 		}
 
 

+ 11 - 7
modules/module_index.go

@@ -232,10 +232,13 @@ func (this *Modules) RegisterModule_Index() *Module {
 					Value: utils.IntToStr(data.A_id),
 					Value: utils.IntToStr(data.A_id),
 				},
 				},
 				{
 				{
-					Kind:    builder.DFKText,
-					Caption: "Page name",
-					Name:    "name",
-					Value:   data.A_name,
+					Kind:     builder.DFKText,
+					Caption:  "Page name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKText,
 					Kind:    builder.DFKText,
@@ -243,6 +246,7 @@ func (this *Modules) RegisterModule_Index() *Module {
 					Name:    "alias",
 					Name:    "alias",
 					Value:   data.A_alias,
 					Value:   data.A_alias,
 					Hint:    "Example: /about-us/ or /about-us.html",
 					Hint:    "Example: /about-us/ or /about-us.html",
+					Max:     "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKTextArea,
 					Kind:    builder.DFKTextArea,
@@ -256,18 +260,21 @@ func (this *Modules) RegisterModule_Index() *Module {
 					Caption: "Meta title",
 					Caption: "Meta title",
 					Name:    "meta_title",
 					Name:    "meta_title",
 					Value:   data.A_meta_title,
 					Value:   data.A_meta_title,
+					Max:     "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKText,
 					Kind:    builder.DFKText,
 					Caption: "Meta keywords",
 					Caption: "Meta keywords",
 					Name:    "meta_keywords",
 					Name:    "meta_keywords",
 					Value:   data.A_meta_keywords,
 					Value:   data.A_meta_keywords,
+					Max:     "255",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKTextArea,
 					Kind:    builder.DFKTextArea,
 					Caption: "Meta description",
 					Caption: "Meta description",
 					Name:    "meta_description",
 					Name:    "meta_description",
 					Value:   data.A_meta_description,
 					Value:   data.A_meta_description,
+					Max:     "510",
 				},
 				},
 				{
 				{
 					Kind:    builder.DFKCheckBox,
 					Kind:    builder.DFKCheckBox,
@@ -275,9 +282,6 @@ func (this *Modules) RegisterModule_Index() *Module {
 					Name:    "active",
 					Name:    "active",
 					Value:   utils.IntToStr(data.A_active),
 					Value:   utils.IntToStr(data.A_active),
 				},
 				},
-				{
-					Kind: builder.DFKMessage,
-				},
 				{
 				{
 					Kind:   builder.DFKSubmit,
 					Kind:   builder.DFKSubmit,
 					Value:  btn_caption,
 					Value:  btn_caption,

+ 7 - 0
modules/module_index_act_cypress.go

@@ -40,6 +40,13 @@ func (this *Modules) RegisterAction_IndexCypressReset() *Action {
 				blog_posts,
 				blog_posts,
 				pages,
 				pages,
 				settings,
 				settings,
+				shop_cat_product_rel,
+				shop_cats,
+				shop_currencies,
+				shop_filter_product_values,
+				shop_filters,
+				shop_filters_values,
+				shop_products,
 				users
 				users
 			;`,
 			;`,
 		)
 		)

+ 444 - 91
modules/module_index_act_mysql_setup.go

@@ -87,47 +87,78 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
+
+		// Table: blog_cat_post_rel
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`INSERT INTO blog_cats (id, user, name, alias, lft, rgt)
-				VALUES
-			(1, 1, 'ROOT', 'ROOT', 1, 24),
-			(2, 1, 'Health and food', 'health-and-food', 2, 15),
-			(3, 1, 'News', 'news', 16, 21),
-			(4, 1, 'Hobby', 'hobby', 22, 23),
-			(5, 1, 'Juices', 'juices', 3, 8),
-			(6, 1, 'Nutrition', 'nutrition', 9, 14),
-			(7, 1, 'Natural', 'natural', 4, 5),
-			(8, 1, 'For kids', 'for-kids', 6, 7),
-			(9, 1, 'For all', 'for-all', 10, 11),
-			(10, 1, 'For athletes', 'for-athletes', 12, 13),
-			(11, 1, 'Computers and technology', 'computers-and-technology', 17, 18),
-			(12, 1, 'Film industry', 'film-industry', 19, 20);`,
+			`CREATE TABLE blog_cat_post_rel (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				post_id int(11) NOT NULL COMMENT 'Post id',
+				category_id int(11) NOT NULL COMMENT 'Category id',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
 		); err != nil {
 		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD UNIQUE KEY alias (alias);`); err != nil {
+
+		// Table: blog_posts
+		if _, err = tx.Exec(
+			`CREATE TABLE blog_posts (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				user int(11) NOT NULL COMMENT 'User id',
+				name varchar(255) NOT NULL COMMENT 'Post name',
+				alias varchar(255) NOT NULL COMMENT 'Post alias',
+				briefly text NOT NULL COMMENT 'Post brief content',
+				content text NOT NULL COMMENT 'Post content',
+				datetime datetime NOT NULL COMMENT 'Creation date/time',
+				active int(1) NOT NULL COMMENT 'Is active post or not',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD KEY lft (lft), ADD KEY rgt (rgt);`); err != nil {
+
+		// Table: pages
+		if _, err = tx.Exec(
+			`CREATE TABLE pages (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				user int(11) NOT NULL COMMENT 'User id',
+				name varchar(255) NOT NULL COMMENT 'Page name',
+				alias varchar(255) NOT NULL COMMENT 'Page url part',
+				content text NOT NULL COMMENT 'Page content',
+				meta_title varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta title',
+				meta_keywords varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta keywords',
+				meta_description varchar(510) NOT NULL DEFAULT '' COMMENT 'Page meta description',
+				datetime datetime NOT NULL COMMENT 'Creation date/time',
+				active int(1) NOT NULL COMMENT 'Is active page or not',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD KEY FK_blog_cats_user (user);`); err != nil {
+
+		// Table: settings
+		if _, err = tx.Exec(
+			`CREATE TABLE settings (
+				name varchar(255) NOT NULL COMMENT 'Setting name',
+				value text NOT NULL COMMENT 'Setting value'
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
 
 
-		// Table: blog_cat_post_rel
+		// Table: shop_cat_product_rel
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`CREATE TABLE blog_cat_post_rel (
+			`CREATE TABLE shop_cat_product_rel (
 				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
 				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-				post_id int(11) NOT NULL COMMENT 'Post id',
+				product_id int(11) NOT NULL COMMENT 'Product id',
 				category_id int(11) NOT NULL COMMENT 'Category id',
 				category_id int(11) NOT NULL COMMENT 'Category id',
 				PRIMARY KEY (id)
 				PRIMARY KEY (id)
 			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
 			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
@@ -136,45 +167,95 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
+
+		// Table: shop_cats
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`INSERT INTO blog_cat_post_rel (id, post_id, category_id) VALUES (1, 1, 9), (2, 2, 12), (3, 3, 8);`,
+			`CREATE TABLE shop_cats (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				user int(11) NOT NULL COMMENT 'User id',
+				name varchar(255) NOT NULL COMMENT 'Category name',
+				alias varchar(255) NOT NULL COMMENT 'Category alias',
+				lft int(11) NOT NULL COMMENT 'For nested set model',
+				rgt int(11) NOT NULL COMMENT 'For nested set model',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
 		); err != nil {
 		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD KEY post_id (post_id), ADD KEY category_id (category_id);`); err != nil {
+
+		// Table: shop_currencies
+		if _, err = tx.Exec(
+			`CREATE TABLE shop_currencies (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				name varchar(255) NOT NULL COMMENT 'Currency name',
+				coefficient float(8,4) NOT NULL DEFAULT '1.0000' COMMENT 'Currency coefficient',
+				code varchar(10) NOT NULL COMMENT 'Currency code',
+				symbol varchar(5) NOT NULL COMMENT 'Currency symbol',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD UNIQUE KEY post_category (post_id,category_id) USING BTREE;`); err != nil {
+
+		// Table: shop_filter_product_values
+		if _, err = tx.Exec(
+			`CREATE TABLE shop_filter_product_values (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				product_id int(11) NOT NULL COMMENT 'Product id',
+				filter_value_id int(11) NOT NULL COMMENT 'Filter value id',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD KEY FK_blog_cat_post_rel_post_id (post_id);`); err != nil {
+
+		// Table: shop_filters
+		if _, err = tx.Exec(
+			`CREATE TABLE shop_filters (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				name varchar(255) NOT NULL COMMENT 'Filter name in CP',
+				filter varchar(255) NOT NULL COMMENT 'Filter name in site',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD KEY FK_blog_cat_post_rel_category_id (category_id);`); err != nil {
+
+		// Table: shop_filters_values
+		if _, err = tx.Exec(
+			`CREATE TABLE shop_filters_values (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				filter_id int(11) NOT NULL COMMENT 'Filter id',
+				name varchar(255) NOT NULL COMMENT 'Value name',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
 
 
-		// Table: blog_posts
+		// Table: shop_products
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`CREATE TABLE blog_posts (
+			`CREATE TABLE shop_products (
 				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
 				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
 				user int(11) NOT NULL COMMENT 'User id',
 				user int(11) NOT NULL COMMENT 'User id',
-				name varchar(255) NOT NULL COMMENT 'Post name',
-				alias varchar(255) NOT NULL COMMENT 'Post alias',
-				briefly text NOT NULL COMMENT 'Post brief content',
-				content text NOT NULL COMMENT 'Post content',
+				currency int(11) NOT NULL COMMENT 'Currency id',
+				price float(8,2) NOT NULL COMMENT 'Product price',
+				name varchar(255) NOT NULL COMMENT 'Product name',
+				alias varchar(255) NOT NULL COMMENT 'Product alias',
+				briefly text NOT NULL COMMENT 'Product brief content',
+				content text NOT NULL COMMENT 'Product content',
 				datetime datetime NOT NULL COMMENT 'Creation date/time',
 				datetime datetime NOT NULL COMMENT 'Creation date/time',
-				active int(1) NOT NULL COMMENT 'Is active post or not',
+				active int(1) NOT NULL COMMENT 'Is active product or not',
 				PRIMARY KEY (id)
 				PRIMARY KEY (id)
 			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
 			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
 		); err != nil {
 		); err != nil {
@@ -182,6 +263,53 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
+
+		// Table: users
+		if _, err = tx.Exec(
+			`CREATE TABLE users (
+				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+				first_name varchar(64) NOT NULL DEFAULT '' COMMENT 'User first name',
+				last_name varchar(64) NOT NULL DEFAULT '' COMMENT 'User last name',
+				email varchar(64) NOT NULL COMMENT 'User email',
+				password varchar(32) NOT NULL COMMENT 'User password (MD5)',
+				admin int(1) NOT NULL COMMENT 'Is admin user or not',
+				active int(1) NOT NULL COMMENT 'Is active user or not',
+				PRIMARY KEY (id)
+			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+		); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		// Demo datas
+		if _, err = tx.Exec(
+			`INSERT INTO blog_cats (id, user, name, alias, lft, rgt)
+				VALUES
+			(1, 1, 'ROOT', 'ROOT', 1, 24),
+			(2, 1, 'Health and food', 'health-and-food', 2, 15),
+			(3, 1, 'News', 'news', 16, 21),
+			(4, 1, 'Hobby', 'hobby', 22, 23),
+			(5, 1, 'Juices', 'juices', 3, 8),
+			(6, 1, 'Nutrition', 'nutrition', 9, 14),
+			(7, 1, 'Natural', 'natural', 4, 5),
+			(8, 1, 'For kids', 'for-kids', 6, 7),
+			(9, 1, 'For all', 'for-all', 10, 11),
+			(10, 1, 'For athletes', 'for-athletes', 12, 13),
+			(11, 1, 'Computers and technology', 'computers-and-technology', 17, 18),
+			(12, 1, 'Film industry', 'film-industry', 19, 20);`,
+		); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(
+			`INSERT INTO blog_cat_post_rel (id, post_id, category_id) VALUES (1, 1, 9), (2, 2, 12), (3, 3, 8);`,
+		); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
 			`INSERT INTO blog_posts SET
 			`INSERT INTO blog_posts SET
 				id = ?,
 				id = ?,
@@ -254,37 +382,6 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_posts ADD UNIQUE KEY alias (alias);`); err != nil {
-			tx.Rollback()
-			wrap.MsgError(err.Error())
-			return
-		}
-		if _, err = tx.Exec(`ALTER TABLE blog_posts ADD KEY FK_blog_posts_user (user);`); err != nil {
-			tx.Rollback()
-			wrap.MsgError(err.Error())
-			return
-		}
-
-		// Table: pages
-		if _, err = tx.Exec(
-			`CREATE TABLE pages (
-				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-				user int(11) NOT NULL COMMENT 'User id',
-				name varchar(255) NOT NULL COMMENT 'Page name',
-				alias varchar(255) NOT NULL COMMENT 'Page url part',
-				content text NOT NULL COMMENT 'Page content',
-				meta_title varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta title',
-				meta_keywords varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta keywords',
-				meta_description varchar(510) NOT NULL DEFAULT '' COMMENT 'Page meta description',
-				datetime datetime NOT NULL COMMENT 'Creation date/time',
-				active int(1) NOT NULL COMMENT 'Is active page or not',
-				PRIMARY KEY (id)
-			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
-		); err != nil {
-			tx.Rollback()
-			wrap.MsgError(err.Error())
-			return
-		}
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
 			`INSERT INTO pages SET
 			`INSERT INTO pages SET
 				id = ?,
 				id = ?,
@@ -351,58 +448,108 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE pages ADD UNIQUE KEY alias (alias);`); err != nil {
+		if _, err = tx.Exec(
+			`INSERT INTO settings (name, value) VALUES ('database_version', '000000003');`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE pages ADD KEY alias_active (alias,active) USING BTREE;`); err != nil {
+		if _, err = tx.Exec(
+			`INSERT INTO shop_cat_product_rel (id, product_id, category_id)
+				VALUES
+			(1, 1, 3);`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE pages ADD KEY FK_pages_user (user);`); err != nil {
+		if _, err = tx.Exec(
+			`INSERT INTO shop_cats (id, user, name, alias, lft, rgt)
+				VALUES
+			(1, 1, 'ROOT', 'ROOT', 1, 6),
+			(2, 1, 'Electronics', 'electronics', 2, 5),
+			(3, 1, 'Mobile phones', 'mobile-phones', 3, 4);`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-
-		// Table: settings
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`CREATE TABLE settings (
-				name varchar(255) NOT NULL COMMENT 'Setting name',
-				value text NOT NULL COMMENT 'Setting value'
-			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+			`INSERT INTO shop_currencies (id, name, coefficient, code, symbol)
+				VALUES
+			(1, 'US Dollar', 1.0000, 'USD', '$');`,
 		); err != nil {
 		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`INSERT INTO settings (name, value) VALUES ('database_version', '000000002');`,
+			`INSERT INTO shop_filter_product_values (id, product_id, filter_value_id)
+				VALUES
+			(1, 1, 3),
+			(2, 1, 7),
+			(3, 1, 9),
+			(4, 1, 10),
+			(5, 1, 11);`,
 		); err != nil {
 		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE settings ADD UNIQUE KEY name (name);`); err != nil {
+		if _, err = tx.Exec(
+			`INSERT INTO shop_filters (id, name, filter)
+				VALUES
+			(1, 'Mobile phones manufacturer', 'Manufacturer'),
+			(2, 'Mobile phones memory', 'Memory'),
+			(3, 'Mobile phones communication standard', 'Communication standard');`,
+		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-
-		// Table: users
 		if _, err = tx.Exec(
 		if _, err = tx.Exec(
-			`CREATE TABLE users (
-				id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-				first_name varchar(64) NOT NULL DEFAULT '' COMMENT 'User first name',
-				last_name varchar(64) NOT NULL DEFAULT '' COMMENT 'User last name',
-				email varchar(64) NOT NULL COMMENT 'User email',
-				password varchar(32) NOT NULL COMMENT 'User password (MD5)',
-				admin int(1) NOT NULL COMMENT 'Is admin user or not',
-				active int(1) NOT NULL COMMENT 'Is active user or not',
-				PRIMARY KEY (id)
-			) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+			`INSERT INTO shop_filters_values (id, filter_id, name)
+				VALUES
+			(1, 1, 'Apple'),
+			(2, 1, 'Asus'),
+			(3, 1, 'Samsung'),
+			(4, 2, '16 Gb'),
+			(5, 2, '32 Gb'),
+			(6, 2, '64 Gb'),
+			(7, 2, '128 Gb'),
+			(8, 2, '256 Gb'),
+			(9, 3, '4G'),
+			(10, 3, '2G'),
+			(11, 3, '3G');`,
+		); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(
+			`INSERT INTO shop_products SET
+				id = ?,
+				user = ?,
+				currency = ?,
+				price = ?,
+				name = ?,
+				alias = ?,
+				briefly = ?,
+				content = ?,
+				datetime = ?,
+				active = ?
+			;`,
+			1,
+			1,
+			1,
+			1000.00,
+			"Samsung Galaxy S10",
+			"samsung-galaxy-s10",
+			"<p>Arcu ac tortor dignissim convallis aenean et tortor. Vitae auctor eu augue ut lectus arcu. Ac turpis egestas integer eget aliquet nibh praesent. Interdum velit euismod in pellentesque massa placerat duis. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Nisl rhoncus mattis rhoncus urna neque viverra justo. Odio ut enim blandit volutpat. Ac auctor augue mauris augue neque gravida. Ut lectus arcu bibendum at varius vel. Porttitor leo a diam sollicitudin tempor id eu nisl nunc. Dolor sit amet consectetur adipiscing elit duis tristique. Semper quis lectus nulla at volutpat diam ut. Sapien eget mi proin sed.</p>",
+			"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Feugiat in ante metus dictum at tempor commodo ullamcorper a. Et malesuada fames ac turpis egestas sed tempus urna et. Euismod elementum nisi quis eleifend. Nisi porta lorem mollis aliquam ut porttitor. Ac turpis egestas maecenas pharetra convallis posuere. Nunc non blandit massa enim nec dui. Commodo elit at imperdiet dui accumsan sit amet nulla. Viverra accumsan in nisl nisi scelerisque. Dui nunc mattis enim ut tellus. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper. Faucibus ornare suspendisse sed nisi lacus. Nulla facilisi morbi tempus iaculis. Ut eu sem integer vitae justo eget magna fermentum iaculis. Ullamcorper sit amet risus nullam eget felis eget nunc. Volutpat sed cras ornare arcu dui vivamus. Eget magna fermentum iaculis eu non diam.</p><p>Arcu ac tortor dignissim convallis aenean et tortor. Vitae auctor eu augue ut lectus arcu. Ac turpis egestas integer eget aliquet nibh praesent. Interdum velit euismod in pellentesque massa placerat duis. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Nisl rhoncus mattis rhoncus urna neque viverra justo. Odio ut enim blandit volutpat. Ac auctor augue mauris augue neque gravida. Ut lectus arcu bibendum at varius vel. Porttitor leo a diam sollicitudin tempor id eu nisl nunc. Dolor sit amet consectetur adipiscing elit duis tristique. Semper quis lectus nulla at volutpat diam ut. Sapien eget mi proin sed.</p>",
+			utils.UnixTimestampToMySqlDateTime(utils.GetCurrentUnixTimestamp()),
+			1,
 		); err != nil {
 		); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
@@ -415,6 +562,133 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
+
+		// Indexes
+		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD UNIQUE KEY post_category (post_id,category_id) USING BTREE;`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD KEY FK_blog_cat_post_rel_post_id (post_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD KEY FK_blog_cat_post_rel_category_id (category_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD UNIQUE KEY alias (alias);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD KEY lft (lft), ADD KEY rgt (rgt);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD KEY FK_blog_cats_user (user);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_posts ADD UNIQUE KEY alias (alias);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE blog_posts ADD KEY FK_blog_posts_user (user);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE pages ADD UNIQUE KEY alias (alias);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE pages ADD KEY alias_active (alias,active) USING BTREE;`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE pages ADD KEY FK_pages_user (user);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE settings ADD UNIQUE KEY name (name);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_cat_product_rel ADD UNIQUE KEY product_category (product_id,category_id) USING BTREE;`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_cat_product_rel ADD KEY FK_shop_cat_product_rel_product_id (product_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_cat_product_rel ADD KEY FK_shop_cat_product_rel_category_id (category_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_cats ADD UNIQUE KEY alias (alias);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_cats ADD KEY lft (lft), ADD KEY rgt (rgt);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_cats ADD KEY FK_shop_cats_user (user);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_filter_product_values ADD UNIQUE KEY product_filter_value (product_id,filter_value_id) USING BTREE;`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_filter_product_values ADD KEY FK_shop_filter_product_values_product_id (product_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_filter_product_values ADD KEY FK_shop_filter_product_values_filter_value_id (filter_value_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_filters_values ADD KEY FK_shop_filters_values_filter_id (filter_id);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_products ADD UNIQUE KEY alias (alias);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_products ADD KEY FK_shop_products_user (user);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`ALTER TABLE shop_products ADD KEY FK_shop_products_currency (currency);`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
 		if _, err = tx.Exec(`ALTER TABLE users ADD UNIQUE KEY email (email);`); err != nil {
 		if _, err = tx.Exec(`ALTER TABLE users ADD UNIQUE KEY email (email);`); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
@@ -422,27 +696,106 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 		}
 		}
 
 
 		// References
 		// References
-		if _, err = tx.Exec(`ALTER TABLE blog_cats ADD CONSTRAINT FK_blog_cats_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;`); err != nil {
+		if _, err = tx.Exec(`
+			ALTER TABLE blog_cat_post_rel ADD CONSTRAINT FK_blog_cat_post_rel_post_id
+			FOREIGN KEY (post_id) REFERENCES blog_posts (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE blog_cat_post_rel ADD CONSTRAINT FK_blog_cat_post_rel_category_id
+			FOREIGN KEY (category_id) REFERENCES blog_cats (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE blog_cats ADD CONSTRAINT FK_blog_cats_user
+			FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE blog_posts ADD CONSTRAINT FK_blog_posts_user
+			FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE pages ADD CONSTRAINT FK_pages_user
+			FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_cat_product_rel ADD CONSTRAINT FK_shop_cat_product_rel_product_id
+			FOREIGN KEY (product_id) REFERENCES shop_products (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_cat_product_rel ADD CONSTRAINT FK_shop_cat_product_rel_category_id
+			FOREIGN KEY (category_id) REFERENCES shop_cats (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_cats ADD CONSTRAINT FK_shop_cats_user
+			FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+		`); err != nil {
+			tx.Rollback()
+			wrap.MsgError(err.Error())
+			return
+		}
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_filter_product_values ADD CONSTRAINT FK_shop_filter_product_values_product_id
+			FOREIGN KEY (product_id) REFERENCES shop_products (id) ON DELETE RESTRICT;
+		`); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD CONSTRAINT FK_blog_cat_post_rel_category_id FOREIGN KEY (category_id) REFERENCES blog_cats (id) ON DELETE RESTRICT;`); err != nil {
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_filter_product_values ADD CONSTRAINT FK_shop_filter_product_values_filter_value_id
+			FOREIGN KEY (filter_value_id) REFERENCES shop_filters_values (id) ON DELETE RESTRICT;
+		`); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_cat_post_rel ADD CONSTRAINT FK_blog_cat_post_rel_post_id FOREIGN KEY (post_id) REFERENCES blog_posts (id) ON DELETE RESTRICT;`); err != nil {
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_filters_values ADD CONSTRAINT FK_shop_filters_values_filter_id
+			FOREIGN KEY (filter_id) REFERENCES shop_filters (id) ON DELETE RESTRICT;
+		`); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE blog_posts ADD CONSTRAINT FK_blog_posts_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;`); err != nil {
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_products ADD CONSTRAINT FK_shop_products_user
+			FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+		`); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return
 		}
 		}
-		if _, err = tx.Exec(`ALTER TABLE pages ADD CONSTRAINT FK_pages_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;`); err != nil {
+		if _, err = tx.Exec(`
+			ALTER TABLE shop_products ADD CONSTRAINT FK_shop_products_currency
+			FOREIGN KEY (currency) REFERENCES shop_currencies (id) ON DELETE RESTRICT;
+		`); err != nil {
 			tx.Rollback()
 			tx.Rollback()
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return

+ 27 - 0
modules/module_settings.go

@@ -92,6 +92,33 @@ func (this *Modules) RegisterModule_Settings() *Module {
 					Required: true,
 					Required: true,
 					Value:    utils.IntToStr((*wrap.Config).Blog.Pagination.Category),
 					Value:    utils.IntToStr((*wrap.Config).Blog.Pagination.Category),
 				},
 				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "",
+					Name:    "",
+					Value:   "",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<hr>`
+					},
+				},
+				{
+					Kind:     builder.DFKNumber,
+					Caption:  "Shop main page",
+					Name:     "shop-index",
+					Min:      "1",
+					Max:      "100",
+					Required: true,
+					Value:    utils.IntToStr((*wrap.Config).Shop.Pagination.Index),
+				},
+				{
+					Kind:     builder.DFKNumber,
+					Caption:  "Shop category page",
+					Name:     "shop-category",
+					Min:      "1",
+					Max:      "100",
+					Required: true,
+					Value:    utils.IntToStr((*wrap.Config).Shop.Pagination.Category),
+				},
 				{
 				{
 					Kind: builder.DFKMessage,
 					Kind: builder.DFKMessage,
 				},
 				},

+ 31 - 0
modules/module_settings_act_pagination.go

@@ -15,6 +15,8 @@ func (this *Modules) RegisterAction_SettingsPagination() *Action {
 	}, func(wrap *wrapper.Wrapper) {
 	}, func(wrap *wrapper.Wrapper) {
 		pf_blog_index := wrap.R.FormValue("blog-index")
 		pf_blog_index := wrap.R.FormValue("blog-index")
 		pf_blog_category := wrap.R.FormValue("blog-category")
 		pf_blog_category := wrap.R.FormValue("blog-category")
+		pf_shop_index := wrap.R.FormValue("shop-index")
+		pf_shop_category := wrap.R.FormValue("shop-category")
 
 
 		if _, err := strconv.Atoi(pf_blog_index); err != nil {
 		if _, err := strconv.Atoi(pf_blog_index); err != nil {
 			wrap.MsgError(`Blog posts count per page on main page must be integer number`)
 			wrap.MsgError(`Blog posts count per page on main page must be integer number`)
@@ -25,9 +27,21 @@ func (this *Modules) RegisterAction_SettingsPagination() *Action {
 			return
 			return
 		}
 		}
 
 
+		if _, err := strconv.Atoi(pf_shop_index); err != nil {
+			wrap.MsgError(`Shop products count per page on main page must be integer number`)
+			return
+		}
+		if _, err := strconv.Atoi(pf_shop_category); err != nil {
+			wrap.MsgError(`Shop products count per page on category page must be integer number`)
+			return
+		}
+
 		pfi_blog_index := utils.StrToInt(pf_blog_index)
 		pfi_blog_index := utils.StrToInt(pf_blog_index)
 		pfi_blog_category := utils.StrToInt(pf_blog_category)
 		pfi_blog_category := utils.StrToInt(pf_blog_category)
 
 
+		pfi_shop_index := utils.StrToInt(pf_shop_index)
+		pfi_shop_category := utils.StrToInt(pf_shop_category)
+
 		// Correct some values
 		// Correct some values
 		if pfi_blog_index < 0 {
 		if pfi_blog_index < 0 {
 			pfi_blog_index = 1
 			pfi_blog_index = 1
@@ -43,9 +57,26 @@ func (this *Modules) RegisterAction_SettingsPagination() *Action {
 			pfi_blog_category = 100
 			pfi_blog_category = 100
 		}
 		}
 
 
+		if pfi_shop_index < 0 {
+			pfi_shop_index = 1
+		}
+		if pfi_shop_index > 100 {
+			pfi_shop_index = 100
+		}
+
+		if pfi_shop_category < 0 {
+			pfi_shop_category = 1
+		}
+		if pfi_shop_category > 100 {
+			pfi_shop_category = 100
+		}
+
 		(*wrap.Config).Blog.Pagination.Index = pfi_blog_index
 		(*wrap.Config).Blog.Pagination.Index = pfi_blog_index
 		(*wrap.Config).Blog.Pagination.Category = pfi_blog_category
 		(*wrap.Config).Blog.Pagination.Category = pfi_blog_category
 
 
+		(*wrap.Config).Shop.Pagination.Index = pfi_shop_index
+		(*wrap.Config).Shop.Pagination.Category = pfi_shop_category
+
 		if err := wrap.ConfigSave(); err != nil {
 		if err := wrap.ConfigSave(); err != nil {
 			wrap.MsgError(err.Error())
 			wrap.MsgError(err.Error())
 			return
 			return

+ 1245 - 0
modules/module_shop.go

@@ -0,0 +1,1245 @@
+package modules
+
+import (
+	"html"
+	"net/http"
+	"strings"
+
+	"golang-fave/assets"
+	"golang-fave/consts"
+	"golang-fave/engine/builder"
+	"golang-fave/engine/fetdata"
+	"golang-fave/engine/sqlw"
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) shop_GetCurrencySelectOptions(wrap *wrapper.Wrapper, id int) string {
+	result := ``
+	rows, err := wrap.DB.Query(
+		`SELECT
+			id,
+			code
+		FROM
+			shop_currencies
+		ORDER BY
+			id ASC
+		;`,
+	)
+	if err == nil {
+		defer rows.Close()
+		values := make([]string, 2)
+		scan := make([]interface{}, len(values))
+		for i := range values {
+			scan[i] = &values[i]
+		}
+		idStr := utils.IntToStr(id)
+		for rows.Next() {
+			err = rows.Scan(scan...)
+			if err == nil {
+				selected := ""
+				if string(values[0]) == idStr {
+					selected = " selected"
+				}
+				result += `<option title="` + html.EscapeString(string(values[1])) + `" value="` + html.EscapeString(string(values[0])) + `"` + selected + `>` + html.EscapeString(string(values[1])) + `</option>`
+			}
+		}
+	}
+	return result
+}
+
+func (this *Modules) shop_GetProductValuesInputs(wrap *wrapper.Wrapper, product_id int) string {
+	result := ``
+	rows, err := wrap.DB.Query(
+		`SELECT
+			shop_filters.id,
+			shop_filters.name,
+			shop_filters_values.id,
+			shop_filters_values.name,
+			IF(shop_filter_product_values.filter_value_id > 0, 1, 0) as selected
+		FROM
+			shop_filters
+			LEFT JOIN shop_filters_values ON shop_filters_values.filter_id = shop_filters.id
+			LEFT JOIN shop_filter_product_values ON shop_filter_product_values.filter_value_id = shop_filters_values.id
+			LEFT JOIN (
+				SELECT
+					shop_filters_values.filter_id,
+					shop_filter_product_values.product_id
+				FROM
+					shop_filter_product_values
+					LEFT JOIN shop_filters_values ON shop_filters_values.id = shop_filter_product_values.filter_value_id 
+				WHERE
+					shop_filter_product_values.product_id = ` + utils.IntToStr(product_id) + `
+				GROUP BY
+					shop_filters_values.filter_id
+			) as filter_used ON filter_used.filter_id = shop_filters.id
+		WHERE
+			(
+				shop_filter_product_values.product_id = ` + utils.IntToStr(product_id) + ` OR
+				shop_filter_product_values.product_id IS NULL
+			) AND
+			filter_used.filter_id IS NOT NULL
+		ORDER BY
+			shop_filters.name ASC,
+			shop_filters_values.name ASC
+		;`,
+	)
+
+	filter_ids := []int{}
+	filter_names := map[int]string{}
+	filter_values := map[int][]string{}
+
+	if err == nil {
+		defer rows.Close()
+		values := make([]string, 5)
+		scan := make([]interface{}, len(values))
+		for i := range values {
+			scan[i] = &values[i]
+		}
+		for rows.Next() {
+			err = rows.Scan(scan...)
+			if err == nil {
+				filter_id := utils.StrToInt(string(values[0]))
+				if !utils.InArrayInt(filter_ids, filter_id) {
+					filter_ids = append(filter_ids, filter_id)
+				}
+				filter_names[filter_id] = html.EscapeString(string(values[1]))
+				selected := ``
+				if utils.StrToInt(string(values[4])) == 1 {
+					selected = ` selected`
+				}
+				filter_values[filter_id] = append(filter_values[filter_id], `<option value="`+html.EscapeString(string(values[2]))+`"`+selected+`>`+html.EscapeString(string(values[3]))+`</option>`)
+			}
+		}
+	}
+	for _, filter_id := range filter_ids {
+		result += `<div class="form-group" id="prod_attr_` + utils.IntToStr(filter_id) + `">` +
+			`<div><b>` + filter_names[filter_id] + `</b></div>` +
+			`<div style="position:relative;">` +
+			`<select class="selectpicker form-control" name="value.` + utils.IntToStr(filter_id) + `" autocomplete="off" required multiple>` +
+			strings.Join(filter_values[filter_id], "") +
+			`</select>` +
+			`<button type="button" class="btn btn-danger" style="position:absolute;top:0px;right:0px;" onclick="fave.ShopProductsRemove(this);">&times;</button>` +
+			`</div>` +
+			`</div>`
+	}
+	return result
+}
+
+func (this *Modules) shop_GetFilterValuesInputs(wrap *wrapper.Wrapper, filter_id int) string {
+	result := ``
+	rows, err := wrap.DB.Query(
+		`SELECT
+			id,
+			name
+		FROM
+			shop_filters_values
+		WHERE
+			filter_id = ?
+		ORDER BY
+			name ASC
+		;`,
+		filter_id,
+	)
+	if err == nil {
+		defer rows.Close()
+		values := make([]string, 2)
+		scan := make([]interface{}, len(values))
+		for i := range values {
+			scan[i] = &values[i]
+		}
+		for rows.Next() {
+			err = rows.Scan(scan...)
+			if err == nil {
+				result += `<div class="form-group" style="position:relative;"><input class="form-control" type="text" name="value.` + html.EscapeString(string(values[0])) + `" value="` + html.EscapeString(string(values[1])) + `" placeholder="" autocomplete="off" required><button type="button" class="btn btn-danger" style="position:absolute;top:0px;right:0px;" onclick="fave.ShopAttributesRemove(this);">&times;</button></div>`
+			}
+		}
+	}
+	return result
+}
+
+func (this *Modules) shop_GetAllAttributesSelectOptions(wrap *wrapper.Wrapper) string {
+	result := ``
+	rows, err := wrap.DB.Query(
+		`SELECT
+			id,
+			name,
+			filter
+		FROM
+			shop_filters
+		ORDER BY
+			name ASC
+		;`,
+	)
+	result += `<option title="&mdash;" value="0">&mdash;</option>`
+	if err == nil {
+		defer rows.Close()
+		values := make([]string, 3)
+		scan := make([]interface{}, len(values))
+		for i := range values {
+			scan[i] = &values[i]
+		}
+		for rows.Next() {
+			err = rows.Scan(scan...)
+			if err == nil {
+				result += `<option title="` + html.EscapeString(string(values[1])) + `" value="` + html.EscapeString(string(values[0])) + `">` + html.EscapeString(string(values[1])) + `</option>`
+			}
+		}
+	}
+	return result
+}
+
+func (this *Modules) shop_GetAllCurrencies(wrap *wrapper.Wrapper) map[int]string {
+	result := map[int]string{}
+	rows, err := wrap.DB.Query(
+		`SELECT
+			id,
+			code
+		FROM
+			shop_currencies
+		ORDER BY
+			id ASC
+		;`,
+	)
+	if err == nil {
+		defer rows.Close()
+		values := make([]string, 2)
+		scan := make([]interface{}, len(values))
+		for i := range values {
+			scan[i] = &values[i]
+		}
+		for rows.Next() {
+			err = rows.Scan(scan...)
+			if err == nil {
+				result[utils.StrToInt(string(values[0]))] = html.EscapeString(string(values[1]))
+			}
+		}
+	}
+	return result
+}
+
+func (this *Modules) RegisterModule_Shop() *Module {
+	return this.newModule(MInfo{
+		WantDB: true,
+		Mount:  "shop",
+		Name:   "Shop",
+		Order:  2,
+		System: false,
+		Icon:   assets.SysSvgIconList,
+		Sub: &[]MISub{
+			{Mount: "default", Name: "List of products", Show: true, Icon: assets.SysSvgIconList},
+			{Mount: "add", Name: "Add new product", Show: true, Icon: assets.SysSvgIconPlus},
+			{Mount: "modify", Name: "Modify product", Show: false},
+			{Sep: true, Show: true},
+			{Mount: "categories", Name: "List of categories", Show: true, Icon: assets.SysSvgIconList},
+			{Mount: "categories-add", Name: "Add new category", Show: true, Icon: assets.SysSvgIconPlus},
+			{Mount: "categories-modify", Name: "Modify category", Show: false},
+			{Sep: true, Show: true},
+			{Mount: "attributes", Name: "List of attributes", Show: true, Icon: assets.SysSvgIconList},
+			{Mount: "attributes-add", Name: "Add new attribute", Show: true, Icon: assets.SysSvgIconPlus},
+			{Mount: "attributes-modify", Name: "Modify attribute", Show: false},
+			{Sep: true, Show: true},
+			{Mount: "currencies", Name: "List of currencies", Show: true, Icon: assets.SysSvgIconList},
+			{Mount: "currencies-add", Name: "Add new currency", Show: true, Icon: assets.SysSvgIconPlus},
+			{Mount: "currencies-modify", Name: "Modify currency", Show: false},
+		},
+	}, func(wrap *wrapper.Wrapper) {
+		if len(wrap.UrlArgs) == 3 && wrap.UrlArgs[0] == "shop" && wrap.UrlArgs[1] == "category" && wrap.UrlArgs[2] != "" {
+			// Shop category
+			row := &utils.MySql_shop_category{}
+			err := wrap.DB.QueryRow(`
+				SELECT
+					id,
+					user,
+					name,
+					alias,
+					lft,
+					rgt
+				FROM
+					shop_cats
+				WHERE
+					alias = ? AND
+					id > 1
+				LIMIT 1;`,
+				wrap.UrlArgs[2],
+			).Scan(
+				&row.A_id,
+				&row.A_user,
+				&row.A_name,
+				&row.A_alias,
+				&row.A_lft,
+				&row.A_rgt,
+			)
+
+			if err != nil && err != wrapper.ErrNoRows {
+				// System error 500
+				utils.SystemErrorPageEngine(wrap.W, err)
+				return
+			} else if err == wrapper.ErrNoRows {
+				// User error 404 page
+				wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
+				return
+			}
+
+			// Fix url
+			if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
+				http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
+				return
+			}
+
+			// Render template
+			wrap.RenderFrontEnd("shop-category", fetdata.New(wrap, row, false), http.StatusOK)
+			return
+		} else if len(wrap.UrlArgs) == 2 && wrap.UrlArgs[0] == "shop" && wrap.UrlArgs[1] != "" {
+			// Shop product
+			row := &utils.MySql_shop_product{}
+			err := wrap.DB.QueryRow(`
+				SELECT
+					id,
+					user,
+					currency,
+					price,
+					name,
+					alias,
+					briefly,
+					content,
+					UNIX_TIMESTAMP(datetime) as datetime,
+					active
+				FROM
+					shop_products
+				WHERE
+					active = 1 and
+					alias = ?
+				LIMIT 1;`,
+				wrap.UrlArgs[1],
+			).Scan(
+				&row.A_id,
+				&row.A_user,
+				&row.A_currency,
+				&row.A_price,
+				&row.A_name,
+				&row.A_alias,
+				&row.A_briefly,
+				&row.A_content,
+				&row.A_datetime,
+				&row.A_active,
+			)
+
+			if err != nil && err != wrapper.ErrNoRows {
+				// System error 500
+				utils.SystemErrorPageEngine(wrap.W, err)
+				return
+			} else if err == wrapper.ErrNoRows {
+				// User error 404 page
+				wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
+				return
+			}
+
+			// Fix url
+			if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
+				http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
+				return
+			}
+
+			// Render template
+			wrap.RenderFrontEnd("shop-product", fetdata.New(wrap, row, false), http.StatusOK)
+			return
+		} else if len(wrap.UrlArgs) == 1 && wrap.UrlArgs[0] == "shop" {
+			// Shop
+
+			// Fix url
+			if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
+				http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
+				return
+			}
+
+			// Render template
+			wrap.RenderFrontEnd("shop", fetdata.New(wrap, nil, false), http.StatusOK)
+			return
+		}
+
+		// User error 404 page
+		wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
+	}, func(wrap *wrapper.Wrapper) (string, string, string) {
+		content := ""
+		sidebar := ""
+		if wrap.CurrSubModule == "" || wrap.CurrSubModule == "default" {
+			content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+				{Name: "List of products"},
+			})
+
+			// Load currencies
+			currencies := this.shop_GetAllCurrencies(wrap)
+
+			content += builder.DataTable(
+				wrap,
+				"shop_products",
+				"id",
+				"DESC",
+				&[]builder.DataTableRow{
+					{
+						DBField: "id",
+					},
+					{
+						DBField:     "name",
+						NameInTable: "Product / URL",
+						CallBack: func(values *[]string) string {
+							name := `<a href="/cp/` + wrap.CurrModule + `/modify/` + (*values)[0] + `/">` + html.EscapeString((*values)[1]) + `</a>`
+							alias := html.EscapeString((*values)[2])
+							return `<div>` + name + `</div><div><small>/shop/` + alias + `/</small></div>`
+						},
+					},
+					{
+						DBField: "alias",
+					},
+					{
+						DBField: "currency",
+					},
+					{
+						DBField:     "price",
+						NameInTable: "Price",
+						Classes:     "d-none d-md-table-cell",
+						CallBack: func(values *[]string) string {
+							return `<div>` + utils.Float64ToStr(utils.StrToFloat64((*values)[4])) + `</div>` +
+								`<div><small>` + currencies[utils.StrToInt((*values)[3])] + `</small></div>`
+						},
+					},
+					{
+						DBField:     "datetime",
+						DBExp:       "UNIX_TIMESTAMP(`datetime`)",
+						NameInTable: "Date / Time",
+						Classes:     "d-none d-lg-table-cell",
+						CallBack: func(values *[]string) string {
+							t := int64(utils.StrToInt((*values)[5]))
+							return `<div>` + utils.UnixTimestampToFormat(t, "02.01.2006") + `</div>` +
+								`<div><small>` + utils.UnixTimestampToFormat(t, "15:04:05") + `</small></div>`
+						},
+					},
+					{
+						DBField:     "active",
+						NameInTable: "Active",
+						Classes:     "d-none d-sm-table-cell",
+						CallBack: func(values *[]string) string {
+							return builder.CheckBox(utils.StrToInt((*values)[6]))
+						},
+					},
+				},
+				func(values *[]string) string {
+					return builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon:   assets.SysSvgIconView,
+							Href:   `/shop/` + (*values)[2] + `/`,
+							Hint:   "View",
+							Target: "_blank",
+						},
+						{
+							Icon: assets.SysSvgIconEdit,
+							Href: "/cp/" + wrap.CurrModule + "/modify/" + (*values)[0] + "/",
+							Hint: "Edit",
+						},
+						{
+							Icon: assets.SysSvgIconRemove,
+							Href: "javascript:fave.ActionDataTableDelete(this,'shop-delete','" +
+								(*values)[0] + "','Are you sure want to delete product?');",
+							Hint:    "Delete",
+							Classes: "delete",
+						},
+					})
+				},
+				"/cp/"+wrap.CurrModule+"/",
+				nil,
+				nil,
+				true,
+			)
+		} else if wrap.CurrSubModule == "categories" {
+			content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+				{Name: "Categories", Link: "/cp/" + wrap.CurrModule + "/" + wrap.CurrSubModule + "/"},
+				{Name: "List of categories"},
+			})
+			content += builder.DataTable(
+				wrap,
+				"shop_cats",
+				"id",
+				"ASC",
+				&[]builder.DataTableRow{
+					{
+						DBField: "id",
+					},
+					{
+						DBField: "user",
+					},
+					{
+						DBField:     "name",
+						NameInTable: "Category",
+						CallBack: func(values *[]string) string {
+							depth := utils.StrToInt((*values)[4]) - 1
+							if depth < 0 {
+								depth = 0
+							}
+							sub := strings.Repeat("&mdash; ", depth)
+							name := `<a href="/cp/` + wrap.CurrModule + `/categories-modify/` + (*values)[0] + `/">` + sub + html.EscapeString((*values)[2]) + `</a>`
+							return `<div>` + name + `</div>`
+						},
+					},
+					{
+						DBField: "alias",
+					},
+					{
+						DBField: "depth",
+					},
+				},
+				func(values *[]string) string {
+					return builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon:   assets.SysSvgIconView,
+							Href:   `/shop/category/` + (*values)[3] + `/`,
+							Hint:   "View",
+							Target: "_blank",
+						},
+						{
+							Icon: assets.SysSvgIconEdit,
+							Href: "/cp/" + wrap.CurrModule + "/categories-modify/" + (*values)[0] + "/",
+							Hint: "Edit",
+						},
+						{
+							Icon: assets.SysSvgIconRemove,
+							Href: "javascript:fave.ActionDataTableDelete(this,'shop-categories-delete','" +
+								(*values)[0] + "','Are you sure want to delete category?');",
+							Hint:    "Delete",
+							Classes: "delete",
+						},
+					})
+				},
+				"/cp/"+wrap.CurrModule+"/"+wrap.CurrSubModule+"/",
+				nil,
+				func(limit_offset int, pear_page int) (*sqlw.Rows, error) {
+					return wrap.DB.Query(
+						`SELECT
+							node.id,
+							node.user,
+							node.name,
+							node.alias,
+							(COUNT(parent.id) - 1) AS depth
+						FROM
+							shop_cats AS node,
+							shop_cats AS parent
+						WHERE
+							node.lft BETWEEN parent.lft AND parent.rgt AND
+							node.id > 1
+						GROUP BY
+							node.id
+						ORDER BY
+							node.lft ASC
+						;`,
+					)
+				},
+				false,
+			)
+		} else if wrap.CurrSubModule == "attributes" {
+			content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+				{Name: "Attributes", Link: "/cp/" + wrap.CurrModule + "/" + wrap.CurrSubModule + "/"},
+				{Name: "List of attributes"},
+			})
+			content += builder.DataTable(
+				wrap,
+				"shop_filters",
+				"id",
+				"DESC",
+				&[]builder.DataTableRow{
+					{
+						DBField: "id",
+					},
+					{
+						DBField:     "name",
+						NameInTable: "Name",
+						CallBack: func(values *[]string) string {
+							name := `<a href="/cp/` + wrap.CurrModule + `/attributes-modify/` + (*values)[0] + `/">` + html.EscapeString((*values)[1]) + `</a>`
+							return `<div>` + name + `</div><div><small>` + html.EscapeString((*values)[2]) + `</small></div>`
+						},
+					},
+					{
+						DBField: "filter",
+					},
+				},
+				func(values *[]string) string {
+					return builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon: assets.SysSvgIconEdit,
+							Href: "/cp/" + wrap.CurrModule + "/attributes-modify/" + (*values)[0] + "/",
+							Hint: "Edit",
+						},
+						{
+							Icon: assets.SysSvgIconRemove,
+							Href: "javascript:fave.ActionDataTableDelete(this,'shop-attributes-delete','" +
+								(*values)[0] + "','Are you sure want to delete attribute?');",
+							Hint:    "Delete",
+							Classes: "delete",
+						},
+					})
+				},
+				"/cp/"+wrap.CurrModule+"/",
+				nil,
+				nil,
+				true,
+			)
+		} else if wrap.CurrSubModule == "currencies" {
+			content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+				{Name: "Currencies", Link: "/cp/" + wrap.CurrModule + "/" + wrap.CurrSubModule + "/"},
+				{Name: "List of currencies"},
+			})
+			content += builder.DataTable(
+				wrap,
+				"shop_currencies",
+				"id",
+				"DESC",
+				&[]builder.DataTableRow{
+					{
+						DBField: "id",
+					},
+					{
+						DBField:     "name",
+						NameInTable: "Name",
+						CallBack: func(values *[]string) string {
+							name := `<a href="/cp/` + wrap.CurrModule + `/currencies-modify/` + (*values)[0] + `/">` + html.EscapeString((*values)[1]) + ` (` + (*values)[3] + `, ` + (*values)[4] + `)</a>`
+							return `<div>` + name + `</div>`
+						},
+					},
+					{
+						DBField:     "coefficient",
+						NameInTable: "Coefficient",
+						Classes:     "d-none d-md-table-cell",
+						CallBack: func(values *[]string) string {
+							return utils.Float64ToStrF(utils.StrToFloat64((*values)[2]), "%.4f")
+						},
+					},
+					{
+						DBField: "code",
+					},
+					{
+						DBField: "symbol",
+					},
+				},
+				func(values *[]string) string {
+					return builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon: assets.SysSvgIconEdit,
+							Href: "/cp/" + wrap.CurrModule + "/currencies-modify/" + (*values)[0] + "/",
+							Hint: "Edit",
+						},
+						{
+							Icon: assets.SysSvgIconRemove,
+							Href: "javascript:fave.ActionDataTableDelete(this,'shop-currencies-delete','" +
+								(*values)[0] + "','Are you sure want to delete currency?');",
+							Hint:    "Delete",
+							Classes: "delete",
+						},
+					})
+				},
+				"/cp/"+wrap.CurrModule+"/",
+				nil,
+				nil,
+				true,
+			)
+		} else if wrap.CurrSubModule == "add" || wrap.CurrSubModule == "modify" {
+			if wrap.CurrSubModule == "add" {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Add new product"},
+				})
+			} else {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Modify product"},
+				})
+			}
+
+			data := utils.MySql_shop_product{
+				A_id:       0,
+				A_user:     0,
+				A_name:     "",
+				A_alias:    "",
+				A_content:  "",
+				A_datetime: 0,
+				A_active:   0,
+			}
+
+			if wrap.CurrSubModule == "modify" {
+				if len(wrap.UrlArgs) != 3 {
+					return "", "", ""
+				}
+				if !utils.IsNumeric(wrap.UrlArgs[2]) {
+					return "", "", ""
+				}
+				err := wrap.DB.QueryRow(`
+					SELECT
+						id,
+						user,
+						currency,
+						price,
+						name,
+						alias,
+						briefly,
+						content,
+						active
+					FROM
+						shop_products
+					WHERE
+						id = ?
+					LIMIT 1;`,
+					utils.StrToInt(wrap.UrlArgs[2]),
+				).Scan(
+					&data.A_id,
+					&data.A_user,
+					&data.A_currency,
+					&data.A_price,
+					&data.A_name,
+					&data.A_alias,
+					&data.A_briefly,
+					&data.A_content,
+					&data.A_active,
+				)
+				if err != nil {
+					return "", "", ""
+				}
+			}
+
+			// All product current categories
+			var selids []int
+			if data.A_id > 0 {
+				rows, err := wrap.DB.Query("SELECT category_id FROM shop_cat_product_rel WHERE product_id = ?;", data.A_id)
+				if err == nil {
+					defer rows.Close()
+					values := make([]int, 1)
+					scan := make([]interface{}, len(values))
+					for i := range values {
+						scan[i] = &values[i]
+					}
+					for rows.Next() {
+						err = rows.Scan(scan...)
+						if err == nil {
+							selids = append(selids, int(values[0]))
+						}
+					}
+				}
+			}
+
+			btn_caption := "Add"
+			if wrap.CurrSubModule == "modify" {
+				btn_caption = "Save"
+			}
+
+			content += builder.DataForm(wrap, []builder.DataFormField{
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "action",
+					Value: "shop-modify",
+				},
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "id",
+					Value: utils.IntToStr(data.A_id),
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Product name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Product price",
+					Name:    "price",
+					Value:   "0",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<div class="form-group n3">` +
+							`<div class="row">` +
+							`<div class="col-md-3">` +
+							`<label for="lbl_price">Product price</label>` +
+							`</div>` +
+							`<div class="col-md-9">` +
+							`<div>` +
+							`<div class="row">` +
+							`<div class="col-md-8">` +
+							`<div><input class="form-control" type="number" step="0.01" id="lbl_price" name="price" value="` + utils.Float64ToStr(data.A_price) + `" placeholder="" autocomplete="off" required></div>` +
+							`<div class="d-md-none mb-3"></div>` +
+							`</div>` +
+							`<div class="col-md-4">` +
+							`<select class="selectpicker form-control" id="lbl_currency" name="currency" data-live-search="true">` +
+							this.shop_GetCurrencySelectOptions(wrap, data.A_currency) +
+							`</select>` +
+							`</div>` +
+							`</div>` +
+							`</div>` +
+							`</div>` +
+							`</div>` +
+							`</div>`
+					},
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Product alias",
+					Name:    "alias",
+					Value:   data.A_alias,
+					Hint:    "Example: mobile-phone",
+					Max:     "255",
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Categories",
+					Name:    "cats",
+					Value:   "0",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<div class="form-group n5">` +
+							`<div class="row">` +
+							`<div class="col-md-3">` +
+							`<label for="lbl_parent">Categories</label>` +
+							`</div>` +
+							`<div class="col-md-9">` +
+							`<div>` +
+							`<select class="selectpicker form-control" id="lbl_cats" name="cats[]" data-live-search="true" multiple>` +
+							this.shop_GetCategorySelectOptions(wrap, 0, 0, selids) +
+							`</select>` +
+							`</div>` +
+							`</div>` +
+							`</div>` +
+							`</div>`
+					},
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Attributes",
+					Name:    "",
+					Value:   "",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<div class="form-group n6">` +
+							`<div class="row">` +
+							`<div class="col-md-3">` +
+							`<label>Attributes</label>` +
+							`</div>` +
+							`<div class="col-md-9">` +
+							`<div class="list-wrapper" style="background:#e9ecef;padding:1rem;border-radius:.25rem;">` +
+							`<div id="list">` +
+							this.shop_GetProductValuesInputs(wrap, data.A_id) +
+							`</div>` +
+							`<div class="list-button" style="position:relative;">` +
+							`<select class="selectpicker form-control" id="lbl_attributes" data-live-search="true">` +
+							this.shop_GetAllAttributesSelectOptions(wrap) +
+							`</select>` +
+							`<button type="button" class="btn btn-success" style="position:absolute;top:0px;right:0px;" onclick="fave.ShopProductsAdd();">Add attribute</button>` +
+							`</div>` +
+							`</div>` +
+							`<div class="d-lg-none" style="height:1rem;"></div>` +
+							`</div>` +
+							`</div>` +
+							`</div>`
+					},
+				},
+				{
+					Kind:    builder.DFKTextArea,
+					Caption: "Briefly",
+					Name:    "briefly",
+					Value:   data.A_briefly,
+					Classes: "briefly wysiwyg",
+				},
+				{
+					Kind:    builder.DFKTextArea,
+					Caption: "Product content",
+					Name:    "content",
+					Value:   data.A_content,
+					Classes: "wysiwyg",
+				},
+				{
+					Kind:    builder.DFKCheckBox,
+					Caption: "Active",
+					Name:    "active",
+					Value:   utils.IntToStr(data.A_active),
+				},
+				{
+					Kind:   builder.DFKSubmit,
+					Value:  btn_caption,
+					Target: "add-edit-button",
+				},
+			})
+
+			if wrap.CurrSubModule == "add" {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Add</button>`
+			} else {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Save</button>`
+			}
+		} else if wrap.CurrSubModule == "categories-add" || wrap.CurrSubModule == "categories-modify" {
+			if wrap.CurrSubModule == "categories-add" {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Categories", Link: "/cp/" + wrap.CurrModule + "/categories/"},
+					{Name: "Add new category"},
+				})
+			} else {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Categories", Link: "/cp/" + wrap.CurrModule + "/categories/"},
+					{Name: "Modify category"},
+				})
+			}
+
+			data := utils.MySql_shop_category{
+				A_id:    0,
+				A_user:  0,
+				A_name:  "",
+				A_alias: "",
+				A_lft:   0,
+				A_rgt:   0,
+			}
+
+			if wrap.CurrSubModule == "categories-modify" {
+				if len(wrap.UrlArgs) != 3 {
+					return "", "", ""
+				}
+				if !utils.IsNumeric(wrap.UrlArgs[2]) {
+					return "", "", ""
+				}
+				err := wrap.DB.QueryRow(`
+					SELECT
+						id,
+						user,
+						name,
+						alias,
+						lft,
+						rgt
+					FROM
+						shop_cats
+					WHERE
+						id = ?
+					LIMIT 1;`,
+					utils.StrToInt(wrap.UrlArgs[2]),
+				).Scan(
+					&data.A_id,
+					&data.A_user,
+					&data.A_name,
+					&data.A_alias,
+					&data.A_lft,
+					&data.A_rgt,
+				)
+				if err != nil {
+					return "", "", ""
+				}
+			}
+
+			btn_caption := "Add"
+			if wrap.CurrSubModule == "categories-modify" {
+				btn_caption = "Save"
+			}
+
+			parentId := 0
+			if wrap.CurrSubModule == "categories-modify" {
+				parentId = this.shop_GetCategoryParentId(wrap, data.A_id)
+			}
+
+			content += builder.DataForm(wrap, []builder.DataFormField{
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "action",
+					Value: "shop-categories-modify",
+				},
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "id",
+					Value: utils.IntToStr(data.A_id),
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Parent",
+					Name:    "parent",
+					Value:   "0",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<div class="form-group n2">` +
+							`<div class="row">` +
+							`<div class="col-md-3">` +
+							`<label for="lbl_parent">Parent</label>` +
+							`</div>` +
+							`<div class="col-md-9">` +
+							`<div>` +
+							`<select class="selectpicker form-control" id="lbl_parent" name="parent" data-live-search="true">` +
+							`<option title="Nothing selected" value="0">&mdash;</option>` +
+							this.shop_GetCategorySelectOptions(wrap, data.A_id, parentId, []int{}) +
+							`</select>` +
+							`</div>` +
+							`</div>` +
+							`</div>` +
+							`</div>`
+					},
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Alias",
+					Name:    "alias",
+					Value:   data.A_alias,
+					Hint:    "Example: popular-products",
+					Max:     "255",
+				},
+				{
+					Kind:   builder.DFKSubmit,
+					Value:  btn_caption,
+					Target: "add-edit-button",
+				},
+			})
+
+			if wrap.CurrSubModule == "categories-add" {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Add</button>`
+			} else {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Save</button>`
+			}
+		} else if wrap.CurrSubModule == "attributes-add" || wrap.CurrSubModule == "attributes-modify" {
+			if wrap.CurrSubModule == "attributes-add" {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Attributes", Link: "/cp/" + wrap.CurrModule + "/attributes/"},
+					{Name: "Add new attribute"},
+				})
+			} else {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Attributes", Link: "/cp/" + wrap.CurrModule + "/attributes/"},
+					{Name: "Modify attribute"},
+				})
+			}
+
+			data := utils.MySql_shop_filter{
+				A_id:     0,
+				A_name:   "",
+				A_filter: "",
+			}
+
+			if wrap.CurrSubModule == "attributes-modify" {
+				if len(wrap.UrlArgs) != 3 {
+					return "", "", ""
+				}
+				if !utils.IsNumeric(wrap.UrlArgs[2]) {
+					return "", "", ""
+				}
+				err := wrap.DB.QueryRow(`
+					SELECT
+						id,
+						name,
+						filter
+					FROM
+						shop_filters
+					WHERE
+						id = ?
+					LIMIT 1;`,
+					utils.StrToInt(wrap.UrlArgs[2]),
+				).Scan(
+					&data.A_id,
+					&data.A_name,
+					&data.A_filter,
+				)
+				if err != nil {
+					return "", "", ""
+				}
+			}
+
+			btn_caption := "Add"
+			if wrap.CurrSubModule == "attributes-modify" {
+				btn_caption = "Save"
+			}
+
+			content += builder.DataForm(wrap, []builder.DataFormField{
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "action",
+					Value: "shop-attributes-modify",
+				},
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "id",
+					Value: utils.IntToStr(data.A_id),
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Attribute name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Attribute in filter",
+					Name:     "filter",
+					Value:    data.A_filter,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Attribute values",
+					Name:    "",
+					Value:   "",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<div class="form-group n4">` +
+							`<div class="row">` +
+							`<div class="col-md-3">` +
+							`<label>Attribute values</label>` +
+							`</div>` +
+							`<div class="col-md-9">` +
+							`<div class="list-wrapper" style="background:#e9ecef;padding:1rem;border-radius:.25rem;">` +
+							`<div id="list">` +
+							this.shop_GetFilterValuesInputs(wrap, data.A_id) +
+							`</div>` +
+							`<div class="list-button"><button type="button" class="btn btn-success" onclick="fave.ShopAttributesAdd();">Add attribute value</button></div>` +
+							`</div>` +
+							`<div class="d-lg-none" style="height:1rem;"></div>` +
+							`</div>` +
+							`</div>` +
+							`</div>`
+					},
+				},
+				{
+					Kind:   builder.DFKSubmit,
+					Value:  btn_caption,
+					Target: "add-edit-button",
+				},
+			})
+
+			if wrap.CurrSubModule == "attributes-add" {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Add</button>`
+			} else {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Save</button>`
+			}
+		} else if wrap.CurrSubModule == "currencies-add" || wrap.CurrSubModule == "currencies-modify" {
+			if wrap.CurrSubModule == "currencies-add" {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Currencies", Link: "/cp/" + wrap.CurrModule + "/currencies/"},
+					{Name: "Add new currency"},
+				})
+			} else {
+				content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
+					{Name: "Currencies", Link: "/cp/" + wrap.CurrModule + "/currencies/"},
+					{Name: "Modify currency"},
+				})
+			}
+
+			data := utils.MySql_shop_currency{
+				A_id:          0,
+				A_name:        "",
+				A_coefficient: 0,
+				A_code:        "",
+				A_symbol:      "",
+			}
+
+			if wrap.CurrSubModule == "currencies-modify" {
+				if len(wrap.UrlArgs) != 3 {
+					return "", "", ""
+				}
+				if !utils.IsNumeric(wrap.UrlArgs[2]) {
+					return "", "", ""
+				}
+				err := wrap.DB.QueryRow(`
+					SELECT
+						id,
+						name,
+						coefficient,
+						code,
+						symbol
+					FROM
+						shop_currencies
+					WHERE
+						id = ?
+					LIMIT 1;`,
+					utils.StrToInt(wrap.UrlArgs[2]),
+				).Scan(
+					&data.A_id,
+					&data.A_name,
+					&data.A_coefficient,
+					&data.A_code,
+					&data.A_symbol,
+				)
+				if err != nil {
+					return "", "", ""
+				}
+			}
+
+			btn_caption := "Add"
+			if wrap.CurrSubModule == "currencies-modify" {
+				btn_caption = "Save"
+			}
+
+			content += builder.DataForm(wrap, []builder.DataFormField{
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "action",
+					Value: "shop-currencies-modify",
+				},
+				{
+					Kind:  builder.DFKHidden,
+					Name:  "id",
+					Value: utils.IntToStr(data.A_id),
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Currency name",
+					Name:     "name",
+					Value:    data.A_name,
+					Required: true,
+					Min:      "1",
+					Max:      "255",
+				},
+				{
+					Kind:    builder.DFKText,
+					Caption: "Currency coefficient",
+					Name:    "coefficient",
+					Value:   "0",
+					CallBack: func(field *builder.DataFormField) string {
+						return `<div class="form-group n3">` +
+							`<div class="row">` +
+							`<div class="col-md-3">` +
+							`<label for="lbl_coefficient">Currency coefficient</label>` +
+							`</div>` +
+							`<div class="col-md-9">` +
+							`<div><input class="form-control" type="number" step="0.0001" id="lbl_coefficient" name="coefficient" value="` + utils.Float64ToStrF(data.A_coefficient, "%.4f") + `" placeholder="" autocomplete="off" required></div>` +
+							`</div>` +
+							`</div>` +
+							`</div>`
+					},
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Currency code",
+					Name:     "code",
+					Value:    data.A_code,
+					Required: true,
+					Min:      "1",
+					Max:      "10",
+				},
+				{
+					Kind:     builder.DFKText,
+					Caption:  "Currency symbol",
+					Name:     "symbol",
+					Value:    data.A_symbol,
+					Required: true,
+					Min:      "1",
+					Max:      "5",
+				},
+				{
+					Kind:   builder.DFKSubmit,
+					Value:  btn_caption,
+					Target: "add-edit-button",
+				},
+			})
+
+			if wrap.CurrSubModule == "currencies-add" {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Add</button>`
+			} else {
+				sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Save</button>`
+			}
+		}
+		return this.getSidebarModules(wrap), content, sidebar
+	})
+}

+ 52 - 0
modules/module_shop_act_delete.go

@@ -0,0 +1,52 @@
+package modules
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopDelete() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-delete",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+
+		if !utils.IsNumeric(pf_id) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+			// Block rows
+			if _, err := tx.Exec("SELECT id FROM shop_products WHERE id = ? FOR UPDATE;", pf_id); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("SELECT id FROM shop_cat_product_rel WHERE product_id = ? FOR UPDATE;", pf_id); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("SELECT id FROM shop_filter_product_values WHERE product_id = ? FOR UPDATE;", pf_id); err != nil {
+				return err
+			}
+
+			// Delete target post with category connection data
+			if _, err := tx.Exec("DELETE FROM shop_filter_product_values WHERE product_id = ?;", pf_id); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("DELETE FROM shop_cat_product_rel WHERE product_id = ?;", pf_id); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("DELETE FROM shop_products WHERE id = ?;", pf_id); err != nil {
+				return err
+			}
+			return nil
+		}); err != nil {
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		// Reload current page
+		wrap.Write(`window.location.reload(false);`)
+	})
+}

+ 59 - 0
modules/module_shop_act_get_attribute_values.go

@@ -0,0 +1,59 @@
+package modules
+
+import (
+	"html"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopGetAttributeValues() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-get-attribute-values",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+
+		if !utils.IsNumeric(pf_id) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		options := ``
+		rows, err := wrap.DB.Query(
+			`SELECT
+				id,
+				name
+			FROM
+				shop_filters_values
+			WHERE
+				filter_id = ?
+			ORDER BY
+				name ASC
+			;`,
+			utils.StrToInt(pf_id),
+		)
+		if err == nil {
+			defer rows.Close()
+			values := make([]string, 2)
+			scan := make([]interface{}, len(values))
+			for i := range values {
+				scan[i] = &values[i]
+			}
+			for rows.Next() {
+				err = rows.Scan(scan...)
+				if err == nil {
+					options += `<option value="` + html.EscapeString(string(values[0])) + `">` + html.EscapeString(utils.JavaScriptVarValue(string(values[1]))) + `</option>`
+				}
+			}
+		}
+
+		wrap.Write(`if($('#prod_attr_` + pf_id + `').length > 0) {`)
+		wrap.Write(`$('#prod_attr_` + pf_id + ` select').prop('disabled', false).prop('multiple', true);`)
+		wrap.Write(`$('#prod_attr_` + pf_id + ` select').html('` + options + `');`)
+		wrap.Write(`$('#prod_attr_` + pf_id + ` select').selectpicker({});`)
+		wrap.Write(`$('#prod_attr_` + pf_id + ` button').prop('disabled', false);`)
+		wrap.Write(`}`)
+	})
+}

+ 271 - 0
modules/module_shop_act_modify.go

@@ -0,0 +1,271 @@
+package modules
+
+import (
+	"errors"
+	"strings"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopModify() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-modify",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+		pf_name := wrap.R.FormValue("name")
+		pf_price := wrap.R.FormValue("price")
+		pf_currency := wrap.R.FormValue("currency")
+		pf_alias := wrap.R.FormValue("alias")
+		pf_briefly := wrap.R.FormValue("briefly")
+		pf_content := wrap.R.FormValue("content")
+		pf_active := wrap.R.FormValue("active")
+
+		if pf_active == "" {
+			pf_active = "0"
+		}
+
+		if !utils.IsNumeric(pf_id) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if !utils.IsFloat(pf_price) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if !utils.IsNumeric(pf_currency) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if pf_name == "" {
+			wrap.MsgError(`Please specify product name`)
+			return
+		}
+
+		if pf_alias == "" {
+			pf_alias = utils.GenerateSingleAlias(pf_name)
+		}
+
+		if !utils.IsValidSingleAlias(pf_alias) {
+			wrap.MsgError(`Please specify correct product alias`)
+			return
+		}
+
+		// Collect fields and data for filter values
+		filter_values := map[int]int{}
+		for key, values := range wrap.R.PostForm {
+			if len(key) > 6 && key[0:6] == "value." {
+				for _, value := range values {
+					if value != "" {
+						filter_values[utils.StrToInt(value)] = utils.StrToInt(key[6:])
+					}
+				}
+			}
+		}
+
+		if pf_id == "0" {
+			if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+				// Insert row
+				res, err := tx.Exec(
+					`INSERT INTO shop_products SET
+						user = ?,
+						currency = ?,
+						price = ?,
+						name = ?,
+						alias = ?,
+						briefly = ?,
+						content = ?,
+						datetime = ?,
+						active = ?
+					;`,
+					wrap.User.A_id,
+					utils.StrToInt(pf_currency),
+					utils.StrToFloat64(pf_price),
+					pf_name,
+					pf_alias,
+					pf_briefly,
+					pf_content,
+					utils.UnixTimestampToMySqlDateTime(utils.GetCurrentUnixTimestamp()),
+					pf_active,
+				)
+				if err != nil {
+					return err
+				}
+
+				// Get inserted product id
+				lastID, err := res.LastInsertId()
+				if err != nil {
+					return err
+				}
+
+				// Block rows
+				if _, err := tx.Exec("SELECT id FROM shop_products WHERE id = ? FOR UPDATE;", lastID); err != nil {
+					return err
+				}
+
+				// Insert product and categories relations
+				catids := utils.GetPostArrayInt("cats[]", wrap.R)
+				if len(catids) > 0 {
+					var catsCount int
+					err = tx.QueryRow(`
+						SELECT
+							COUNT(*)
+						FROM
+							shop_cats
+						WHERE
+							id IN(` + strings.Join(utils.ArrayOfIntToArrayOfString(catids), ",") + `)
+						FOR UPDATE;`,
+					).Scan(
+						&catsCount,
+					)
+					if err != nil {
+						return err
+					}
+					if len(catids) != catsCount {
+						return errors.New("Inner system error")
+					}
+					var bulkInsertArr []string
+					for _, el := range catids {
+						bulkInsertArr = append(bulkInsertArr, `(NULL,`+utils.Int64ToStr(lastID)+`,`+utils.IntToStr(el)+`)`)
+					}
+					if _, err = tx.Exec(
+						`INSERT INTO shop_cat_product_rel (id,product_id,category_id) VALUES ` + strings.Join(bulkInsertArr, ",") + `;`,
+					); err != nil {
+						return err
+					}
+				}
+
+				// Insert product and filter values relations
+				for vid, _ := range filter_values {
+					if _, err = tx.Exec(
+						`INSERT INTO shop_filter_product_values SET
+							product_id = ?,
+							filter_value_id = ?
+						;`,
+						lastID,
+						vid,
+					); err != nil {
+						return err
+					}
+				}
+				return nil
+			}); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+
+			wrap.Write(`window.location='/cp/shop/';`)
+		} else {
+			if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+				// Block rows
+				if _, err := tx.Exec("SELECT id FROM shop_products WHERE id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("SELECT id FROM shop_currencies WHERE id = ? FOR UPDATE;", utils.StrToInt(pf_currency)); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("SELECT id FROM shop_cat_product_rel WHERE product_id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("SELECT id FROM shop_filter_product_values WHERE product_id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+					return err
+				}
+
+				// Update row
+				if _, err := tx.Exec(
+					`UPDATE shop_products SET
+						currency = ?,
+						price = ?,
+						name = ?,
+						alias = ?,
+						briefly = ?,
+						content = ?,
+						active = ?
+					WHERE
+						id = ?
+					;`,
+					utils.StrToInt(pf_currency),
+					utils.StrToFloat64(pf_price),
+					pf_name,
+					pf_alias,
+					pf_briefly,
+					pf_content,
+					pf_active,
+					utils.StrToInt(pf_id),
+				); err != nil {
+					return err
+				}
+
+				// Delete product and categories relations
+				if _, err := tx.Exec("DELETE FROM shop_cat_product_rel WHERE product_id = ?;", pf_id); err != nil {
+					return err
+				}
+
+				// Insert product and categories relations
+				catids := utils.GetPostArrayInt("cats[]", wrap.R)
+				if len(catids) > 0 {
+					var catsCount int
+					err := tx.QueryRow(`
+						SELECT
+							COUNT(*)
+						FROM
+							shop_cats
+						WHERE
+							id IN(` + strings.Join(utils.ArrayOfIntToArrayOfString(catids), ",") + `)
+						FOR UPDATE;`,
+					).Scan(
+						&catsCount,
+					)
+					if err != nil {
+						return err
+					}
+					if len(catids) != catsCount {
+						return errors.New("Inner system error")
+					}
+					var bulkInsertArr []string
+					for _, el := range catids {
+						bulkInsertArr = append(bulkInsertArr, `(NULL,`+pf_id+`,`+utils.IntToStr(el)+`)`)
+					}
+					if _, err := tx.Exec(
+						`INSERT INTO shop_cat_product_rel (id,product_id,category_id) VALUES ` + strings.Join(bulkInsertArr, ",") + `;`,
+					); err != nil {
+						return err
+					}
+				}
+
+				// Delete product and filter values relations
+				if _, err := tx.Exec(
+					`DELETE FROM shop_filter_product_values WHERE product_id = ?;`,
+					utils.StrToInt(pf_id),
+				); err != nil {
+					return err
+				}
+
+				// Insert product and filter values relations
+				for vid, _ := range filter_values {
+					if _, err := tx.Exec(
+						`INSERT INTO shop_filter_product_values SET
+							product_id = ?,
+							filter_value_id = ?
+						;`,
+						utils.StrToInt(pf_id),
+						vid,
+					); err != nil {
+						return err
+					}
+				}
+				return nil
+			}); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+
+			wrap.Write(`window.location='/cp/shop/modify/` + pf_id + `/';`)
+		}
+	})
+}

+ 82 - 0
modules/module_shop_attributes_act_delete.go

@@ -0,0 +1,82 @@
+package modules
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopAttributesDelete() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-attributes-delete",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+
+		if !utils.IsNumeric(pf_id) || utils.StrToInt(pf_id) <= 1 {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+			// Block rows
+			if _, err := tx.Exec("SELECT id FROM shop_filters WHERE id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("SELECT id FROM shop_filters_values WHERE filter_id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+				return err
+			}
+			if _, err := tx.Exec(
+				`SELECT
+					shop_filter_product_values.id
+				FROM
+					shop_filter_product_values
+					LEFT JOIN shop_filters_values ON shop_filters_values.id = shop_filter_product_values.filter_value_id
+				WHERE
+					shop_filters_values.id IS NOT NULL AND
+					shop_filters_values.filter_id = ?
+				FOR UPDATE;`,
+				utils.StrToInt(pf_id),
+			); err != nil {
+				return err
+			}
+
+			// Process
+			if _, err := tx.Exec(
+				`DELETE
+					shop_filter_product_values
+				FROM
+					shop_filter_product_values
+					LEFT JOIN shop_filters_values ON shop_filters_values.id = shop_filter_product_values.filter_value_id
+				WHERE
+					shop_filters_values.id IS NOT NULL AND
+					shop_filters_values.filter_id = ?
+				;`,
+				utils.StrToInt(pf_id),
+			); err != nil {
+				return err
+			}
+			if _, err := tx.Exec(
+				`DELETE FROM shop_filters_values WHERE filter_id = ?;`,
+				utils.StrToInt(pf_id),
+			); err != nil {
+				return err
+			}
+			if _, err := tx.Exec(
+				`DELETE FROM shop_filters WHERE id = ?;`,
+				utils.StrToInt(pf_id),
+			); err != nil {
+				return err
+			}
+			return nil
+		})
+
+		if err != nil {
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		// Reload current page
+		wrap.Write(`window.location.reload(false);`)
+	})
+}

+ 201 - 0
modules/module_shop_attributes_act_modify.go

@@ -0,0 +1,201 @@
+package modules
+
+import (
+	"strings"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopAttributesModify() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-attributes-modify",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+		pf_name := wrap.R.FormValue("name")
+		pf_filter := wrap.R.FormValue("filter")
+
+		if !utils.IsNumeric(pf_id) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if pf_name == "" {
+			wrap.MsgError(`Please specify attribute name`)
+			return
+		}
+
+		if pf_filter == "" {
+			wrap.MsgError(`Please specify attribute in filter`)
+			return
+		}
+
+		// Collect fields and data
+		filter_values := map[string]int{}
+		for key, values := range wrap.R.PostForm {
+			if len(key) > 6 && key[0:6] == "value." {
+				for _, value := range values {
+					if value != "" {
+						filter_values[value] = utils.StrToInt(key[6:])
+					}
+				}
+			}
+		}
+
+		if pf_id == "0" {
+			if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+				// Insert row
+				res, err := tx.Exec(
+					`INSERT INTO shop_filters SET
+						name = ?,
+						filter = ?
+					;`,
+					pf_name,
+					pf_filter,
+				)
+				if err != nil {
+					return err
+				}
+
+				// Get inserted id
+				lastID, err := res.LastInsertId()
+				if err != nil {
+					return err
+				}
+
+				// Block rows
+				if _, err := tx.Exec("SELECT id FROM shop_filters WHERE id = ? FOR UPDATE;", lastID); err != nil {
+					return err
+				}
+
+				// Insert values
+				for vname, _ := range filter_values {
+					if _, err = tx.Exec(
+						`INSERT INTO shop_filters_values SET
+							filter_id = ?,
+							name = ?
+						;`,
+						lastID,
+						vname,
+					); err != nil {
+						return err
+					}
+				}
+				return nil
+			}); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+
+			wrap.Write(`window.location='/cp/shop/attributes/';`)
+		} else {
+			if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+				// Block rows
+				if _, err := tx.Exec("SELECT id FROM shop_filters WHERE id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("SELECT id FROM shop_filters_values WHERE filter_id = ? FOR UPDATE;", utils.StrToInt(pf_id)); err != nil {
+					return err
+				}
+				if _, err := tx.Exec(
+					`SELECT
+						shop_filter_product_values.id
+					FROM
+						shop_filter_product_values
+						LEFT JOIN shop_filters_values ON shop_filters_values.id = shop_filter_product_values.filter_value_id
+					WHERE
+						shop_filters_values.id IS NOT NULL AND
+						shop_filters_values.filter_id = ?
+					FOR UPDATE;`,
+					utils.StrToInt(pf_id),
+				); err != nil {
+					return err
+				}
+
+				// Update row
+				if _, err := tx.Exec(
+					`UPDATE shop_filters SET
+						name = ?,
+						filter = ?
+					WHERE
+						id = ?
+					;`,
+					pf_name,
+					pf_filter,
+					utils.StrToInt(pf_id),
+				); err != nil {
+					return err
+				}
+
+				// Delete not existed rows
+				ignore_ids := []string{}
+				for _, vid := range filter_values {
+					if vid != 0 {
+						ignore_ids = append(ignore_ids, utils.IntToStr(vid))
+					}
+				}
+				if len(ignore_ids) > 0 {
+					if _, err := tx.Exec(
+						`DELETE
+							shop_filter_product_values
+						FROM
+							shop_filter_product_values
+							LEFT JOIN shop_filters_values ON shop_filters_values.id = shop_filter_product_values.filter_value_id
+						WHERE
+							shop_filters_values.id IS NOT NULL AND
+							shop_filters_values.filter_id = ? AND
+							shop_filter_product_values.filter_value_id NOT IN (`+strings.Join(ignore_ids, ",")+`)
+						;`,
+						utils.StrToInt(pf_id),
+					); err != nil {
+						return err
+					}
+					if _, err := tx.Exec(
+						`DELETE FROM shop_filters_values WHERE filter_id = ? AND id NOT IN (`+strings.Join(ignore_ids, ",")+`);`,
+						utils.StrToInt(pf_id),
+					); err != nil {
+						return err
+					}
+				}
+
+				// Insert new values, update existed rows
+				for vname, vid := range filter_values {
+					if vid == 0 {
+						if _, err := tx.Exec(
+							`INSERT INTO shop_filters_values SET
+								filter_id = ?,
+								name = ?
+							;`,
+							utils.StrToInt(pf_id),
+							vname,
+						); err != nil {
+							return err
+						}
+					} else {
+						if _, err := tx.Exec(
+							`UPDATE shop_filters_values SET
+								name = ?
+							WHERE
+								id = ? AND
+								filter_id = ?
+							;`,
+							vname,
+							vid,
+							utils.StrToInt(pf_id),
+						); err != nil {
+							return err
+						}
+					}
+				}
+				return nil
+			}); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+
+			wrap.Write(`window.location='/cp/shop/attributes-modify/` + pf_id + `/';`)
+		}
+	})
+}

+ 91 - 0
modules/module_shop_categories.go

@@ -0,0 +1,91 @@
+package modules
+
+import (
+	"html"
+	"strings"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) shop_GetCategorySelectOptions(wrap *wrapper.Wrapper, id int, parentId int, selids []int) string {
+	result := ``
+	rows, err := wrap.DB.Query(
+		`SELECT
+			node.id,
+			node.user,
+			node.name,
+			node.alias,
+			(COUNT(parent.id) - 1) AS depth
+		FROM
+			shop_cats AS node,
+			shop_cats AS parent
+		WHERE
+			node.lft BETWEEN parent.lft AND parent.rgt AND
+			node.id > 1
+		GROUP BY
+			node.id
+		ORDER BY
+			node.lft ASC
+		;`,
+	)
+	if err == nil {
+		defer rows.Close()
+		values := make([]string, 5)
+		scan := make([]interface{}, len(values))
+		for i := range values {
+			scan[i] = &values[i]
+		}
+		idStr := utils.IntToStr(id)
+		parentIdStr := utils.IntToStr(parentId)
+		for rows.Next() {
+			err = rows.Scan(scan...)
+			if err == nil {
+				disabled := ""
+				if string(values[0]) == idStr {
+					disabled = " disabled"
+				}
+				selected := ""
+				if string(values[0]) == parentIdStr {
+					selected = " selected"
+				}
+				if len(selids) > 0 && utils.InArrayInt(selids, utils.StrToInt(string(values[0]))) {
+					selected = " selected"
+				}
+				depth := utils.StrToInt(string(values[4])) - 1
+				if depth < 0 {
+					depth = 0
+				}
+				sub := strings.Repeat("&mdash; ", depth)
+				result += `<option title="` + html.EscapeString(string(values[2])) + `" value="` + html.EscapeString(string(values[0])) + `"` + disabled + selected + `>` + sub + html.EscapeString(string(values[2])) + `</option>`
+			}
+		}
+	}
+	return result
+}
+
+func (this *Modules) shop_GetCategoryParentId(wrap *wrapper.Wrapper, id int) int {
+	var parentId int
+	err := wrap.DB.QueryRow(`
+		SELECT
+			parent.id
+		FROM
+			shop_cats AS node,
+			shop_cats AS parent
+		WHERE
+			node.lft BETWEEN parent.lft AND parent.rgt AND
+			node.id = ? AND
+			parent.id <> ?
+		ORDER BY
+			parent.lft DESC
+		LIMIT 1;`,
+		id,
+		id,
+	).Scan(
+		&parentId,
+	)
+	if err != nil {
+		return 0
+	}
+	return parentId
+}

+ 60 - 0
modules/module_shop_categories_act_delete.go

@@ -0,0 +1,60 @@
+package modules
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopCategoriesDelete() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-categories-delete",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+
+		if !utils.IsNumeric(pf_id) || utils.StrToInt(pf_id) <= 1 {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+			// Block rows
+			if _, err := tx.Exec("SELECT id FROM shop_cats FOR UPDATE;"); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("SELECT id FROM shop_cat_product_rel WHERE category_id = ? FOR UPDATE;", pf_id); err != nil {
+				return err
+			}
+
+			// Process
+			if _, err := tx.Exec("SELECT @ml := lft, @mr := rgt FROM shop_cats WHERE id = ?;", pf_id); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("DELETE FROM shop_cats WHERE id = ?;", pf_id); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("UPDATE shop_cats SET lft = lft - 1, rgt = rgt - 1 WHERE lft > @ml AND rgt < @mr;"); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("UPDATE shop_cats SET lft = lft - 2 WHERE lft > @mr;"); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("UPDATE shop_cats SET rgt = rgt - 2 WHERE rgt > @mr;"); err != nil {
+				return err
+			}
+			if _, err := tx.Exec("DELETE FROM shop_cat_product_rel WHERE category_id = ?;", pf_id); err != nil {
+				return err
+			}
+			return nil
+		})
+
+		if err != nil {
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		// Reload current page
+		wrap.Write(`window.location.reload(false);`)
+	})
+}

+ 227 - 0
modules/module_shop_categories_act_modify.go

@@ -0,0 +1,227 @@
+package modules
+
+import (
+	"errors"
+
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) shop_ActionCategoryAdd(wrap *wrapper.Wrapper, pf_id, pf_name, pf_alias, pf_parent string) error {
+	return wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+		// Block rows
+		if _, err := tx.Exec("SELECT id FROM shop_cats FOR UPDATE;"); err != nil {
+			return err
+		}
+
+		// Process
+		if _, err := tx.Exec("SELECT @mr := rgt FROM shop_cats WHERE id = ?;", pf_parent); err != nil {
+			return err
+		}
+		if _, err := tx.Exec("UPDATE shop_cats SET rgt = rgt + 2 WHERE rgt > @mr;"); err != nil {
+			return err
+		}
+		if _, err := tx.Exec("UPDATE shop_cats SET lft = lft + 2 WHERE lft > @mr;"); err != nil {
+			return err
+		}
+		if _, err := tx.Exec("UPDATE shop_cats SET rgt = rgt + 2 WHERE id = ?;", pf_parent); err != nil {
+			return err
+		}
+		if _, err := tx.Exec("INSERT INTO shop_cats (id, user, name, alias, lft, rgt) VALUES (NULL, ?, ?, ?, @mr, @mr + 1);", wrap.User.A_id, pf_name, pf_alias); err != nil {
+			return err
+		}
+		return nil
+	})
+}
+
+func (this *Modules) shop_ActionCategoryUpdate(wrap *wrapper.Wrapper, pf_id, pf_name, pf_alias, pf_parent string) error {
+	parentId := this.shop_GetCategoryParentId(wrap, utils.StrToInt(pf_id))
+
+	if utils.StrToInt(pf_parent) == parentId {
+		// If parent not changed, just update category data
+		return wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+			// Process
+			if _, err := tx.Exec(`
+				UPDATE shop_cats SET
+					name = ?,
+					alias = ?
+				WHERE
+					id > 1 AND
+					id = ?
+				;`,
+				pf_name,
+				pf_alias,
+				pf_id,
+			); err != nil {
+				return err
+			}
+			return nil
+		})
+	}
+
+	// TODO: Fix parent change
+
+	// Parent is changed, move category to new parent
+	return wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+		// Block all rows
+		if _, err := tx.Exec("SELECT id FROM shop_cats FOR UPDATE;"); err != nil {
+			return err
+		}
+
+		var parentL int
+		var parentR int
+		if err := tx.QueryRow(`SELECT lft, rgt FROM shop_cats WHERE id = ?;`, pf_parent).Scan(&parentL, &parentR); err != nil {
+			return err
+		}
+
+		var targetL int
+		var targetR int
+		if err := tx.QueryRow(`SELECT lft, rgt FROM shop_cats WHERE id = ?;`, pf_id).Scan(&targetL, &targetR); err != nil {
+			return err
+		}
+
+		if !(targetL < parentL && targetR > parentR) {
+			// Select data
+			rows, err := tx.Query("SELECT id, lft, rgt FROM shop_cats WHERE lft >= ? and rgt <= ? ORDER BY lft ASC", targetL, targetR)
+			if err != nil {
+				return err
+			}
+			defer rows.Close()
+			var rows_id []int
+			var rows_lft []int
+			var rows_rgt []int
+			for rows.Next() {
+				var row_id int
+				var row_lft int
+				var row_rgt int
+				if err := rows.Scan(&row_id, &row_lft, &row_rgt); err == nil {
+					rows_id = append(rows_id, row_id)
+					rows_lft = append(rows_lft, row_lft)
+					rows_rgt = append(rows_rgt, row_rgt)
+				}
+			}
+
+			if targetL > parentR {
+				// From right to left
+				// Shift
+				step := targetR - targetL + 1
+				if _, err := tx.Exec("UPDATE shop_cats SET lft = lft + ? WHERE lft > ? and lft < ?;", step, parentR, targetL); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("UPDATE shop_cats SET rgt = rgt + ? WHERE rgt > ? and rgt < ?;", step, parentR, targetL); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("UPDATE shop_cats SET rgt = rgt + ? WHERE id = ?;", step, pf_parent); err != nil {
+					return err
+				}
+
+				// Update target rows
+				for i, _ := range rows_id {
+					new_lft := rows_lft[i] - (targetL - parentR)
+					new_rgt := rows_rgt[i] - (targetL - parentR)
+					if _, err := tx.Exec("UPDATE shop_cats SET lft = ?, rgt = ? WHERE id = ?;", new_lft, new_rgt, rows_id[i]); err != nil {
+						return err
+					}
+				}
+			} else {
+				// From left to right
+				// Shift
+				step := targetR - targetL + 1
+				if _, err := tx.Exec("UPDATE shop_cats SET lft = lft - ? WHERE lft > ? and lft < ?;", step, targetR, parentR); err != nil {
+					return err
+				}
+				if _, err := tx.Exec("UPDATE shop_cats SET rgt = rgt - ? WHERE rgt > ? and rgt < ?;", step, targetR, parentR); err != nil {
+					return err
+				}
+
+				// Update target rows
+				for i, _ := range rows_id {
+					new_lft := rows_lft[i] + (parentR - targetL - step)
+					new_rgt := rows_rgt[i] + (parentR - targetL - step)
+					if _, err := tx.Exec("UPDATE shop_cats SET lft = ?, rgt = ? WHERE id = ?;", new_lft, new_rgt, rows_id[i]); err != nil {
+						return err
+					}
+				}
+			}
+		} else {
+			// Trying to move category to they child as parent
+			return errors.New("Category can't be moved inside here child")
+		}
+
+		// Update target cat data
+		if _, err := tx.Exec("UPDATE shop_cats SET name = ?, alias = ? WHERE id = ?;", pf_name, pf_alias, pf_id); err != nil {
+			return err
+		}
+
+		return nil
+	})
+}
+
+func (this *Modules) RegisterAction_ShopCategoriesModify() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-categories-modify",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+		pf_name := wrap.R.FormValue("name")
+		pf_alias := wrap.R.FormValue("alias")
+		pf_parent := wrap.R.FormValue("parent")
+
+		if !utils.IsNumeric(pf_id) || !utils.IsNumeric(pf_parent) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if pf_name == "" {
+			wrap.MsgError(`Please specify category name`)
+			return
+		}
+
+		if pf_alias == "" {
+			pf_alias = utils.GenerateSingleAlias(pf_name)
+		}
+
+		if !utils.IsValidSingleAlias(pf_alias) {
+			wrap.MsgError(`Please specify correct category alias`)
+			return
+		}
+
+		// Set root category as default
+		if pf_parent == "0" {
+			pf_parent = "1"
+		} else {
+			// Check if parent category exists
+			var parentId int
+			err := wrap.DB.QueryRow(`
+				SELECT
+					id
+				FROM
+					shop_cats
+				WHERE
+					id > 1 AND
+					id = ?
+				LIMIT 1;`,
+				pf_parent,
+			).Scan(&parentId)
+			if err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+		}
+
+		if pf_id == "0" {
+			if err := this.shop_ActionCategoryAdd(wrap, pf_id, pf_name, pf_alias, pf_parent); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+			wrap.Write(`window.location='/cp/shop/categories/';`)
+		} else {
+			if err := this.shop_ActionCategoryUpdate(wrap, pf_id, pf_name, pf_alias, pf_parent); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+			wrap.Write(`window.location='/cp/shop/categories-modify/` + pf_id + `/';`)
+		}
+	})
+}

+ 40 - 0
modules/module_shop_currencies_act_delete.go

@@ -0,0 +1,40 @@
+package modules
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopCurrenciesDelete() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-currencies-delete",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+
+		if !utils.IsNumeric(pf_id) || utils.StrToInt(pf_id) <= 1 {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+			// Process
+			if _, err := tx.Exec(
+				`DELETE FROM shop_currencies WHERE id = ?;`,
+				utils.StrToInt(pf_id),
+			); err != nil {
+				return err
+			}
+			return nil
+		})
+
+		if err != nil {
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		// Reload current page
+		wrap.Write(`window.location.reload(false);`)
+	})
+}

+ 104 - 0
modules/module_shop_currencies_act_modify.go

@@ -0,0 +1,104 @@
+package modules
+
+import (
+	"golang-fave/engine/wrapper"
+	"golang-fave/utils"
+)
+
+func (this *Modules) RegisterAction_ShopCurrenciesModify() *Action {
+	return this.newAction(AInfo{
+		WantDB:    true,
+		Mount:     "shop-currencies-modify",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_id := wrap.R.FormValue("id")
+		pf_name := wrap.R.FormValue("name")
+		pf_coefficient := wrap.R.FormValue("coefficient")
+		pf_code := wrap.R.FormValue("code")
+		pf_symbol := wrap.R.FormValue("symbol")
+
+		if !utils.IsNumeric(pf_id) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if pf_name == "" {
+			wrap.MsgError(`Please specify currency name`)
+			return
+		}
+
+		if !utils.IsFloat(pf_coefficient) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if pf_code == "" {
+			wrap.MsgError(`Please specify currency code`)
+			return
+		}
+
+		if pf_symbol == "" {
+			wrap.MsgError(`Please specify currency symbol`)
+			return
+		}
+
+		if pf_id == "0" {
+			if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+				// Insert row
+				_, err := tx.Exec(
+					`INSERT INTO shop_currencies SET
+						name = ?,
+						coefficient = ?,
+						code = ?,
+						symbol = ?
+					;`,
+					pf_name,
+					pf_coefficient,
+					pf_code,
+					pf_symbol,
+				)
+				if err != nil {
+					return err
+				}
+				return nil
+			}); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+
+			wrap.Write(`window.location='/cp/shop/currencies/';`)
+		} else {
+			if err := wrap.DB.Transaction(func(tx *wrapper.Tx) error {
+				// Block rows
+				if _, err := tx.Exec("SELECT id FROM shop_currencies WHERE id = ? FOR UPDATE;", pf_id); err != nil {
+					return err
+				}
+
+				// Update row
+				if _, err := tx.Exec(
+					`UPDATE shop_currencies SET
+						name = ?,
+						coefficient = ?,
+						code = ?,
+						symbol = ?
+					WHERE
+						id = ?
+					;`,
+					pf_name,
+					pf_coefficient,
+					pf_code,
+					pf_symbol,
+					utils.StrToInt(pf_id),
+				); err != nil {
+					return err
+				}
+				return nil
+			}); err != nil {
+				wrap.MsgError(err.Error())
+				return
+			}
+
+			wrap.Write(`window.location='/cp/shop/currencies-modify/` + pf_id + `/';`)
+		}
+	})
+}

+ 1 - 1
modules/modules.go

@@ -146,7 +146,7 @@ func (this *Modules) getSidebarModuleSubMenu(wrap *wrapper.Wrapper, mod *MInfo)
 			if item.Show {
 			if item.Show {
 				if !item.Sep {
 				if !item.Sep {
 					class := ""
 					class := ""
-					if (item.Mount == "default" && len(wrap.UrlArgs) <= 1) || (len(wrap.UrlArgs) >= 2 && item.Mount == wrap.UrlArgs[1]) {
+					if (item.Mount == "default" && len(wrap.UrlArgs) <= 1) || (len(wrap.UrlArgs) >= 2 && item.Mount == wrap.UrlArgs[1]) || (len(wrap.UrlArgs) >= 2 && item.Mount == "default" && wrap.UrlArgs[1] == "modify") || (len(wrap.UrlArgs) >= 2 && len(strings.Split(item.Mount, "-")) <= 1 && len(strings.Split(wrap.UrlArgs[1], "-")) >= 2 && strings.Split(wrap.UrlArgs[1], "-")[1] == "modify" && strings.Split(item.Mount, "-")[0] == strings.Split(wrap.UrlArgs[1], "-")[0]) {
 						class = " active"
 						class = " active"
 					}
 					}
 					icon := item.Icon
 					icon := item.Icon

+ 2 - 0
support/migrate/000000001.go

@@ -7,4 +7,6 @@ import (
 var Migrations = map[string]func(*sqlw.DB) error{
 var Migrations = map[string]func(*sqlw.DB) error{
 	"000000000": nil,
 	"000000000": nil,
 	"000000001": nil,
 	"000000001": nil,
+	"000000002": Migrate_000000002,
+	"000000003": Migrate_000000003,
 }
 }

+ 10 - 0
support/migrate/000000002.go

@@ -0,0 +1,10 @@
+package migrate
+
+import (
+	"golang-fave/engine/sqlw"
+)
+
+func Migrate_000000002(db *sqlw.DB) error {
+	// Empty migration file
+	return nil
+}

+ 293 - 0
support/migrate/000000003.go

@@ -0,0 +1,293 @@
+package migrate
+
+import (
+	"golang-fave/engine/sqlw"
+	"golang-fave/utils"
+)
+
+func Migrate_000000003(db *sqlw.DB) error {
+	// Remove blog indexes
+	if _, err := db.Exec(`DROP INDEX post_id ON blog_cat_post_rel`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`DROP INDEX category_id ON blog_cat_post_rel`); err != nil {
+		return err
+	}
+
+	// Table: shop_cat_product_rel
+	if _, err := db.Exec(
+		`CREATE TABLE shop_cat_product_rel (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			product_id int(11) NOT NULL COMMENT 'Product id',
+			category_id int(11) NOT NULL COMMENT 'Category id',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Table: shop_cats
+	if _, err := db.Exec(
+		`CREATE TABLE shop_cats (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			user int(11) NOT NULL COMMENT 'User id',
+			name varchar(255) NOT NULL COMMENT 'Category name',
+			alias varchar(255) NOT NULL COMMENT 'Category alias',
+			lft int(11) NOT NULL COMMENT 'For nested set model',
+			rgt int(11) NOT NULL COMMENT 'For nested set model',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Table: shop_currencies
+	if _, err := db.Exec(
+		`CREATE TABLE shop_currencies (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			name varchar(255) NOT NULL COMMENT 'Currency name',
+			coefficient float(8,4) NOT NULL DEFAULT '1.0000' COMMENT 'Currency coefficient',
+			code varchar(10) NOT NULL COMMENT 'Currency code',
+			symbol varchar(5) NOT NULL COMMENT 'Currency symbol',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Table: shop_filter_product_values
+	if _, err := db.Exec(
+		`CREATE TABLE shop_filter_product_values (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			product_id int(11) NOT NULL COMMENT 'Product id',
+			filter_value_id int(11) NOT NULL COMMENT 'Filter value id',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Table: shop_filters
+	if _, err := db.Exec(
+		`CREATE TABLE shop_filters (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			name varchar(255) NOT NULL COMMENT 'Filter name in CP',
+			filter varchar(255) NOT NULL COMMENT 'Filter name in site',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Table: shop_filters_values
+	if _, err := db.Exec(
+		`CREATE TABLE shop_filters_values (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			filter_id int(11) NOT NULL COMMENT 'Filter id',
+			name varchar(255) NOT NULL COMMENT 'Value name',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Table: shop_products
+	if _, err := db.Exec(
+		`CREATE TABLE shop_products (
+			id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+			user int(11) NOT NULL COMMENT 'User id',
+			currency int(11) NOT NULL COMMENT 'Currency id',
+			price float(8,2) NOT NULL COMMENT 'Product price',
+			name varchar(255) NOT NULL COMMENT 'Product name',
+			alias varchar(255) NOT NULL COMMENT 'Product alias',
+			briefly text NOT NULL COMMENT 'Product brief content',
+			content text NOT NULL COMMENT 'Product content',
+			datetime datetime NOT NULL COMMENT 'Creation date/time',
+			active int(1) NOT NULL COMMENT 'Is active product or not',
+			PRIMARY KEY (id)
+		) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
+	); err != nil {
+		return err
+	}
+
+	// Demo datas
+	if _, err := db.Exec(
+		`INSERT INTO shop_cat_product_rel (id, product_id, category_id)
+			VALUES
+		(1, 1, 3);`,
+	); err != nil {
+		return err
+	}
+	if _, err := db.Exec(
+		`INSERT INTO shop_cats (id, user, name, alias, lft, rgt)
+			VALUES
+		(1, 1, 'ROOT', 'ROOT', 1, 6),
+		(2, 1, 'Electronics', 'electronics', 2, 5),
+		(3, 1, 'Mobile phones', 'mobile-phones', 3, 4);`,
+	); err != nil {
+		return err
+	}
+	if _, err := db.Exec(
+		`INSERT INTO shop_currencies (id, name, coefficient, code, symbol)
+			VALUES
+		(1, 'US Dollar', 1.0000, 'USD', '$');`,
+	); err != nil {
+		return err
+	}
+	if _, err := db.Exec(
+		`INSERT INTO shop_filter_product_values (id, product_id, filter_value_id)
+			VALUES
+		(1, 1, 3),
+		(2, 1, 7),
+		(3, 1, 9),
+		(4, 1, 10),
+		(5, 1, 11);`,
+	); err != nil {
+		return err
+	}
+	if _, err := db.Exec(
+		`INSERT INTO shop_filters (id, name, filter)
+			VALUES
+		(1, 'Mobile phones manufacturer', 'Manufacturer'),
+		(2, 'Mobile phones memory', 'Memory'),
+		(3, 'Mobile phones communication standard', 'Communication standard');`,
+	); err != nil {
+		return err
+	}
+	if _, err := db.Exec(
+		`INSERT INTO shop_filters_values (id, filter_id, name)
+			VALUES
+		(1, 1, 'Apple'),
+		(2, 1, 'Asus'),
+		(3, 1, 'Samsung'),
+		(4, 2, '16 Gb'),
+		(5, 2, '32 Gb'),
+		(6, 2, '64 Gb'),
+		(7, 2, '128 Gb'),
+		(8, 2, '256 Gb'),
+		(9, 3, '4G'),
+		(10, 3, '2G'),
+		(11, 3, '3G');`,
+	); err != nil {
+		return err
+	}
+	if _, err := db.Exec(
+		`INSERT INTO shop_products SET
+			id = ?,
+			user = ?,
+			currency = ?,
+			price = ?,
+			name = ?,
+			alias = ?,
+			briefly = ?,
+			content = ?,
+			datetime = ?,
+			active = ?
+		;`,
+		1,
+		1,
+		1,
+		1000.00,
+		"Samsung Galaxy S10",
+		"samsung-galaxy-s10",
+		"<p>Arcu ac tortor dignissim convallis aenean et tortor. Vitae auctor eu augue ut lectus arcu. Ac turpis egestas integer eget aliquet nibh praesent. Interdum velit euismod in pellentesque massa placerat duis. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Nisl rhoncus mattis rhoncus urna neque viverra justo. Odio ut enim blandit volutpat. Ac auctor augue mauris augue neque gravida. Ut lectus arcu bibendum at varius vel. Porttitor leo a diam sollicitudin tempor id eu nisl nunc. Dolor sit amet consectetur adipiscing elit duis tristique. Semper quis lectus nulla at volutpat diam ut. Sapien eget mi proin sed.</p>",
+		"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Feugiat in ante metus dictum at tempor commodo ullamcorper a. Et malesuada fames ac turpis egestas sed tempus urna et. Euismod elementum nisi quis eleifend. Nisi porta lorem mollis aliquam ut porttitor. Ac turpis egestas maecenas pharetra convallis posuere. Nunc non blandit massa enim nec dui. Commodo elit at imperdiet dui accumsan sit amet nulla. Viverra accumsan in nisl nisi scelerisque. Dui nunc mattis enim ut tellus. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper. Faucibus ornare suspendisse sed nisi lacus. Nulla facilisi morbi tempus iaculis. Ut eu sem integer vitae justo eget magna fermentum iaculis. Ullamcorper sit amet risus nullam eget felis eget nunc. Volutpat sed cras ornare arcu dui vivamus. Eget magna fermentum iaculis eu non diam.</p><p>Arcu ac tortor dignissim convallis aenean et tortor. Vitae auctor eu augue ut lectus arcu. Ac turpis egestas integer eget aliquet nibh praesent. Interdum velit euismod in pellentesque massa placerat duis. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Nisl rhoncus mattis rhoncus urna neque viverra justo. Odio ut enim blandit volutpat. Ac auctor augue mauris augue neque gravida. Ut lectus arcu bibendum at varius vel. Porttitor leo a diam sollicitudin tempor id eu nisl nunc. Dolor sit amet consectetur adipiscing elit duis tristique. Semper quis lectus nulla at volutpat diam ut. Sapien eget mi proin sed.</p>",
+		utils.UnixTimestampToMySqlDateTime(utils.GetCurrentUnixTimestamp()),
+		1,
+	); err != nil {
+		return err
+	}
+
+	// Indexes
+	if _, err := db.Exec(`ALTER TABLE shop_cat_product_rel ADD UNIQUE KEY product_category (product_id,category_id) USING BTREE;`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_cat_product_rel ADD KEY FK_shop_cat_product_rel_product_id (product_id);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_cat_product_rel ADD KEY FK_shop_cat_product_rel_category_id (category_id);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_cats ADD UNIQUE KEY alias (alias);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_cats ADD KEY lft (lft), ADD KEY rgt (rgt);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_cats ADD KEY FK_shop_cats_user (user);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_filter_product_values ADD UNIQUE KEY product_filter_value (product_id,filter_value_id) USING BTREE;`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_filter_product_values ADD KEY FK_shop_filter_product_values_product_id (product_id);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_filter_product_values ADD KEY FK_shop_filter_product_values_filter_value_id (filter_value_id);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_filters_values ADD KEY FK_shop_filters_values_filter_id (filter_id);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_products ADD UNIQUE KEY alias (alias);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_products ADD KEY FK_shop_products_user (user);`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`ALTER TABLE shop_products ADD KEY FK_shop_products_currency (currency);`); err != nil {
+		return err
+	}
+
+	// References
+	if _, err := db.Exec(`
+		ALTER TABLE shop_cat_product_rel ADD CONSTRAINT FK_shop_cat_product_rel_product_id
+		FOREIGN KEY (product_id) REFERENCES shop_products (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_cat_product_rel ADD CONSTRAINT FK_shop_cat_product_rel_category_id
+		FOREIGN KEY (category_id) REFERENCES shop_cats (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_cats ADD CONSTRAINT FK_shop_cats_user
+		FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_filter_product_values ADD CONSTRAINT FK_shop_filter_product_values_product_id
+		FOREIGN KEY (product_id) REFERENCES shop_products (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_filter_product_values ADD CONSTRAINT FK_shop_filter_product_values_filter_value_id
+		FOREIGN KEY (filter_value_id) REFERENCES shop_filters_values (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_filters_values ADD CONSTRAINT FK_shop_filters_values_filter_id
+		FOREIGN KEY (filter_id) REFERENCES shop_filters (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_products ADD CONSTRAINT FK_shop_products_user
+		FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+	if _, err := db.Exec(`
+		ALTER TABLE shop_products ADD CONSTRAINT FK_shop_products_currency
+		FOREIGN KEY (currency) REFERENCES shop_currencies (id) ON DELETE RESTRICT;
+	`); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 143 - 72
support/schema.sql

@@ -1,80 +1,151 @@
-# Tables with keys
-CREATE TABLE `blog_cats` (
-	`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-	`user` int(11) NOT NULL COMMENT 'User id',
-	`name` varchar(255) NOT NULL COMMENT 'Category name',
-	`alias` varchar(255) NOT NULL COMMENT 'Category alias',
-	`lft` int(11) NOT NULL COMMENT 'For nested set model',
-	`rgt` int(11) NOT NULL COMMENT 'For nested set model',
-	PRIMARY KEY (`id`)
+# Tables
+CREATE TABLE blog_cats (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	user int(11) NOT NULL COMMENT 'User id',
+	name varchar(255) NOT NULL COMMENT 'Category name',
+	alias varchar(255) NOT NULL COMMENT 'Category alias',
+	lft int(11) NOT NULL COMMENT 'For nested set model',
+	rgt int(11) NOT NULL COMMENT 'For nested set model',
+	PRIMARY KEY (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-ALTER TABLE `blog_cats` ADD UNIQUE KEY `alias` (`alias`);
-ALTER TABLE `blog_cats` ADD KEY `lft` (`lft`), ADD KEY `rgt` (`rgt`);
-ALTER TABLE `blog_cats` ADD KEY `FK_blog_cats_user` (`user`);
-
-CREATE TABLE `blog_cat_post_rel` (
-	`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-	`post_id` int(11) NOT NULL COMMENT 'Post id',
-	`category_id` int(11) NOT NULL COMMENT 'Category id',
-	PRIMARY KEY (`id`)
+CREATE TABLE blog_cat_post_rel (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	post_id int(11) NOT NULL COMMENT 'Post id',
+	category_id int(11) NOT NULL COMMENT 'Category id',
+	PRIMARY KEY (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-ALTER TABLE `blog_cat_post_rel` ADD KEY `post_id` (`post_id`), ADD KEY `category_id` (`category_id`);
-ALTER TABLE `blog_cat_post_rel` ADD UNIQUE KEY `post_category` (`post_id`,`category_id`) USING BTREE;
-ALTER TABLE `blog_cat_post_rel` ADD KEY `FK_blog_cat_post_rel_post_id` (`post_id`);
-ALTER TABLE `blog_cat_post_rel` ADD KEY `FK_blog_cat_post_rel_category_id` (`category_id`);
-
-CREATE TABLE `blog_posts` (
-	`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-	`user` int(11) NOT NULL COMMENT 'User id',
-	`name` varchar(255) NOT NULL COMMENT 'Post name',
-	`alias` varchar(255) NOT NULL COMMENT 'Post alias',
-	`briefly` text NOT NULL COMMENT 'Post brief content',
-	`content` text NOT NULL COMMENT 'Post content',
-	`datetime` datetime NOT NULL COMMENT 'Creation date/time',
-	`active` int(1) NOT NULL COMMENT 'Is active post or not',
-	PRIMARY KEY (`id`)
+CREATE TABLE blog_posts (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	user int(11) NOT NULL COMMENT 'User id',
+	name varchar(255) NOT NULL COMMENT 'Post name',
+	alias varchar(255) NOT NULL COMMENT 'Post alias',
+	briefly text NOT NULL COMMENT 'Post brief content',
+	content text NOT NULL COMMENT 'Post content',
+	datetime datetime NOT NULL COMMENT 'Creation date/time',
+	active int(1) NOT NULL COMMENT 'Is active post or not',
+	PRIMARY KEY (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-ALTER TABLE `blog_posts` ADD UNIQUE KEY `alias` (`alias`);
-ALTER TABLE `blog_posts` ADD KEY `FK_blog_posts_user` (`user`);
-
-CREATE TABLE `pages` (
-	`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-	`user` int(11) NOT NULL COMMENT 'User id',
-	`name` varchar(255) NOT NULL COMMENT 'Page name',
-	`alias` varchar(255) NOT NULL COMMENT 'Page url part',
-	`content` text NOT NULL COMMENT 'Page content',
-	`meta_title` varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta title',
-	`meta_keywords` varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta keywords',
-	`meta_description` varchar(510) NOT NULL DEFAULT '' COMMENT 'Page meta description',
-	`datetime` datetime NOT NULL COMMENT 'Creation date/time',
-	`active` int(1) NOT NULL COMMENT 'Is active page or not',
-	PRIMARY KEY (`id`)
+CREATE TABLE pages (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	user int(11) NOT NULL COMMENT 'User id',
+	name varchar(255) NOT NULL COMMENT 'Page name',
+	alias varchar(255) NOT NULL COMMENT 'Page url part',
+	content text NOT NULL COMMENT 'Page content',
+	meta_title varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta title',
+	meta_keywords varchar(255) NOT NULL DEFAULT '' COMMENT 'Page meta keywords',
+	meta_description varchar(510) NOT NULL DEFAULT '' COMMENT 'Page meta description',
+	datetime datetime NOT NULL COMMENT 'Creation date/time',
+	active int(1) NOT NULL COMMENT 'Is active page or not',
+	PRIMARY KEY (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-ALTER TABLE `pages` ADD UNIQUE KEY `alias` (`alias`);
-ALTER TABLE `pages` ADD KEY `alias_active` (`alias`,`active`) USING BTREE;
-ALTER TABLE `pages` ADD KEY `FK_pages_user` (`user`);
-
-CREATE TABLE `settings` (
-	`name` varchar(255) NOT NULL COMMENT 'Setting name',
-	`value` text NOT NULL COMMENT 'Setting value',
+CREATE TABLE settings (
+	name varchar(255) NOT NULL COMMENT 'Setting name',
+	value text NOT NULL COMMENT 'Setting value'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-ALTER TABLE `settings` ADD UNIQUE KEY `name` (`name`);
-
-CREATE TABLE `users` (
-	`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
-	`first_name` varchar(64) NOT NULL DEFAULT '' COMMENT 'User first name',
-	`last_name` varchar(64) NOT NULL DEFAULT '' COMMENT 'User last name',
-	`email` varchar(64) NOT NULL COMMENT 'User email',
-	`password` varchar(32) NOT NULL COMMENT 'User password (MD5)',
-	`admin` int(1) NOT NULL COMMENT 'Is admin user or not',
-	`active` int(1) NOT NULL COMMENT 'Is active user or not',
-	PRIMARY KEY (`id`)
+CREATE TABLE shop_cat_product_rel (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	product_id int(11) NOT NULL COMMENT 'Product id',
+	category_id int(11) NOT NULL COMMENT 'Category id',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE shop_cats (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	user int(11) NOT NULL COMMENT 'User id',
+	name varchar(255) NOT NULL COMMENT 'Category name',
+	alias varchar(255) NOT NULL COMMENT 'Category alias',
+	lft int(11) NOT NULL COMMENT 'For nested set model',
+	rgt int(11) NOT NULL COMMENT 'For nested set model',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE shop_currencies (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	name varchar(255) NOT NULL COMMENT 'Currency name',
+	coefficient float(8,4) NOT NULL DEFAULT '1.0000' COMMENT 'Currency coefficient',
+	code varchar(10) NOT NULL COMMENT 'Currency code',
+	symbol varchar(5) NOT NULL COMMENT 'Currency symbol',
+	PRIMARY KEY (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-ALTER TABLE `users` ADD UNIQUE KEY `email` (`email`);
+CREATE TABLE shop_filter_product_values (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	product_id int(11) NOT NULL COMMENT 'Product id',
+	filter_value_id int(11) NOT NULL COMMENT 'Filter value id',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE shop_filters (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	name varchar(255) NOT NULL COMMENT 'Filter name in CP',
+	filter varchar(255) NOT NULL COMMENT 'Filter name in site',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE shop_filters_values (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	filter_id int(11) NOT NULL COMMENT 'Filter id',
+	name varchar(255) NOT NULL COMMENT 'Value name',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE shop_products (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	user int(11) NOT NULL COMMENT 'User id',
+	currency int(11) NOT NULL COMMENT 'Currency id',
+	price float(8,2) NOT NULL COMMENT 'Product price',
+	name varchar(255) NOT NULL COMMENT 'Product name',
+	alias varchar(255) NOT NULL COMMENT 'Product alias',
+	briefly text NOT NULL COMMENT 'Product brief content',
+	content text NOT NULL COMMENT 'Product content',
+	datetime datetime NOT NULL COMMENT 'Creation date/time',
+	active int(1) NOT NULL COMMENT 'Is active product or not',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE users (
+	id int(11) NOT NULL AUTO_INCREMENT COMMENT 'AI',
+	first_name varchar(64) NOT NULL DEFAULT '' COMMENT 'User first name',
+	last_name varchar(64) NOT NULL DEFAULT '' COMMENT 'User last name',
+	email varchar(64) NOT NULL COMMENT 'User email',
+	password varchar(32) NOT NULL COMMENT 'User password (MD5)',
+	admin int(1) NOT NULL COMMENT 'Is admin user or not',
+	active int(1) NOT NULL COMMENT 'Is active user or not',
+	PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+# Indexes
+ALTER TABLE blog_cat_post_rel ADD UNIQUE KEY post_category (post_id,category_id) USING BTREE;
+ALTER TABLE blog_cat_post_rel ADD KEY FK_blog_cat_post_rel_post_id (post_id);
+ALTER TABLE blog_cat_post_rel ADD KEY FK_blog_cat_post_rel_category_id (category_id);
+ALTER TABLE blog_cats ADD UNIQUE KEY alias (alias);
+ALTER TABLE blog_cats ADD KEY lft (lft), ADD KEY rgt (rgt);
+ALTER TABLE blog_cats ADD KEY FK_blog_cats_user (user);
+ALTER TABLE blog_posts ADD UNIQUE KEY alias (alias);
+ALTER TABLE blog_posts ADD KEY FK_blog_posts_user (user);
+ALTER TABLE pages ADD UNIQUE KEY alias (alias);
+ALTER TABLE pages ADD KEY alias_active (alias,active) USING BTREE;
+ALTER TABLE pages ADD KEY FK_pages_user (user);
+ALTER TABLE settings ADD UNIQUE KEY name (name);
+ALTER TABLE shop_cat_product_rel ADD UNIQUE KEY product_category (product_id,category_id) USING BTREE;
+ALTER TABLE shop_cat_product_rel ADD KEY FK_shop_cat_product_rel_product_id (product_id);
+ALTER TABLE shop_cat_product_rel ADD KEY FK_shop_cat_product_rel_category_id (category_id);
+ALTER TABLE shop_cats ADD UNIQUE KEY alias (alias);
+ALTER TABLE shop_cats ADD KEY lft (lft), ADD KEY rgt (rgt);
+ALTER TABLE shop_cats ADD KEY FK_shop_cats_user (user);
+ALTER TABLE shop_filter_product_values ADD UNIQUE KEY product_filter_value (product_id,filter_value_id) USING BTREE;
+ALTER TABLE shop_filter_product_values ADD KEY FK_shop_filter_product_values_product_id (product_id);
+ALTER TABLE shop_filter_product_values ADD KEY FK_shop_filter_product_values_filter_value_id (filter_value_id);
+ALTER TABLE shop_filters_values ADD KEY FK_shop_filters_values_filter_id (filter_id);
+ALTER TABLE shop_products ADD UNIQUE KEY alias (alias);
+ALTER TABLE shop_products ADD KEY FK_shop_products_user (user);
+ALTER TABLE shop_products ADD KEY FK_shop_products_currency (currency);
+ALTER TABLE users ADD UNIQUE KEY email (email);
 
 
 # References
 # References
-ALTER TABLE `blog_cats` ADD CONSTRAINT `FK_blog_cats_user` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE RESTRICT;
-ALTER TABLE `blog_cat_post_rel` ADD CONSTRAINT `FK_blog_cat_post_rel_category_id` FOREIGN KEY (`category_id`) REFERENCES `blog_cats` (`id`) ON DELETE RESTRICT;
-ALTER TABLE `blog_cat_post_rel` ADD CONSTRAINT `FK_blog_cat_post_rel_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog_posts` (`id`) ON DELETE RESTRICT;
-ALTER TABLE `blog_posts` ADD CONSTRAINT `FK_blog_posts_user` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE RESTRICT;
-ALTER TABLE `pages` ADD CONSTRAINT `FK_pages_user` FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE RESTRICT;
+ALTER TABLE blog_cat_post_rel ADD CONSTRAINT FK_blog_cat_post_rel_post_id FOREIGN KEY (post_id) REFERENCES blog_posts (id) ON DELETE RESTRICT;
+ALTER TABLE blog_cat_post_rel ADD CONSTRAINT FK_blog_cat_post_rel_category_id FOREIGN KEY (category_id) REFERENCES blog_cats (id) ON DELETE RESTRICT;
+ALTER TABLE blog_cats ADD CONSTRAINT FK_blog_cats_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+ALTER TABLE blog_posts ADD CONSTRAINT FK_blog_posts_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+ALTER TABLE pages ADD CONSTRAINT FK_pages_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+ALTER TABLE shop_cat_product_rel ADD CONSTRAINT FK_shop_cat_product_rel_product_id FOREIGN KEY (product_id) REFERENCES shop_products (id) ON DELETE RESTRICT;
+ALTER TABLE shop_cat_product_rel ADD CONSTRAINT FK_shop_cat_product_rel_category_id FOREIGN KEY (category_id) REFERENCES shop_cats (id) ON DELETE RESTRICT;
+ALTER TABLE shop_cats ADD CONSTRAINT FK_shop_cats_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+ALTER TABLE shop_filter_product_values ADD CONSTRAINT FK_shop_filter_product_values_product_id FOREIGN KEY (product_id) REFERENCES shop_products (id) ON DELETE RESTRICT;
+ALTER TABLE shop_filter_product_values ADD CONSTRAINT FK_shop_filter_product_values_filter_value_id FOREIGN KEY (filter_value_id) REFERENCES shop_filters_values (id) ON DELETE RESTRICT;
+ALTER TABLE shop_filters_values ADD CONSTRAINT FK_shop_filters_values_filter_id FOREIGN KEY (filter_id) REFERENCES shop_filters (id) ON DELETE RESTRICT;
+ALTER TABLE shop_products ADD CONSTRAINT FK_shop_products_user FOREIGN KEY (user) REFERENCES users (id) ON DELETE RESTRICT;
+ALTER TABLE shop_products ADD CONSTRAINT FK_shop_products_currency FOREIGN KEY (currency) REFERENCES shop_currencies (id) ON DELETE RESTRICT;

+ 10 - 0
utils/mysql_struct_shop_category.go

@@ -0,0 +1,10 @@
+package utils
+
+type MySql_shop_category struct {
+	A_id    int
+	A_user  int
+	A_name  string
+	A_alias string
+	A_lft   int
+	A_rgt   int
+}

+ 9 - 0
utils/mysql_struct_shop_currency.go

@@ -0,0 +1,9 @@
+package utils
+
+type MySql_shop_currency struct {
+	A_id          int
+	A_name        string
+	A_coefficient float64
+	A_code        string
+	A_symbol      string
+}

+ 7 - 0
utils/mysql_struct_shop_filter.go

@@ -0,0 +1,7 @@
+package utils
+
+type MySql_shop_filter struct {
+	A_id     int
+	A_name   string
+	A_filter string
+}

+ 14 - 0
utils/mysql_struct_shop_post.go

@@ -0,0 +1,14 @@
+package utils
+
+type MySql_shop_product struct {
+	A_id       int
+	A_user     int
+	A_currency int
+	A_price    float64
+	A_name     string
+	A_alias    string
+	A_briefly  string
+	A_content  string
+	A_datetime int
+	A_active   int
+}

+ 29 - 0
utils/utils.go

@@ -51,6 +51,13 @@ func IsNumeric(str string) bool {
 	return false
 	return false
 }
 }
 
 
+func IsFloat(str string) bool {
+	if _, err := strconv.ParseFloat(str, 64); err == nil {
+		return true
+	}
+	return false
+}
+
 func IsValidEmail(email string) bool {
 func IsValidEmail(email string) bool {
 	regexpe := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
 	regexpe := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
 	return regexpe.MatchString(email)
 	return regexpe.MatchString(email)
@@ -69,6 +76,12 @@ func IsValidAlias(alias string) bool {
 		return false
 		return false
 	}
 	}
 
 
+	// Shop module
+	regexpeShop := regexp.MustCompile(`^\/shop\/`)
+	if alias == "/shop" || regexpeShop.MatchString(alias) {
+		return false
+	}
+
 	regexpeSlash := regexp.MustCompile(`[\/]{2,}`)
 	regexpeSlash := regexp.MustCompile(`[\/]{2,}`)
 	regexpeChars := regexp.MustCompile(`^\/([a-zA-Z0-9\/\-_\.]+)\/?$`)
 	regexpeChars := regexp.MustCompile(`^\/([a-zA-Z0-9\/\-_\.]+)\/?$`)
 	return (!regexpeSlash.MatchString(alias) && regexpeChars.MatchString(alias)) || alias == "/"
 	return (!regexpeSlash.MatchString(alias) && regexpeChars.MatchString(alias)) || alias == "/"
@@ -253,6 +266,22 @@ func StrToInt(str string) int {
 	return 0
 	return 0
 }
 }
 
 
+func Float64ToStr(num float64) string {
+	return fmt.Sprintf("%.2f", num)
+}
+
+func Float64ToStrF(num float64, format string) string {
+	return fmt.Sprintf(format, num)
+}
+
+func StrToFloat64(str string) float64 {
+	num, err := strconv.ParseFloat(str, 64)
+	if err == nil {
+		return num
+	}
+	return 0
+}
+
 func GenerateAlias(str string) string {
 func GenerateAlias(str string) string {
 	if str == "" {
 	if str == "" {
 		return ""
 		return ""

+ 33 - 0
utils/utils_test.go

@@ -41,6 +41,13 @@ func TestIsNumeric(t *testing.T) {
 	Expect(t, IsNumeric("string"), false)
 	Expect(t, IsNumeric("string"), false)
 }
 }
 
 
+func TestIsFloat(t *testing.T) {
+	Expect(t, IsFloat("12345"), true)
+	Expect(t, IsFloat("1.23"), true)
+	Expect(t, IsFloat("1,23"), false)
+	Expect(t, IsFloat("string"), false)
+}
+
 func TestIsValidEmail(t *testing.T) {
 func TestIsValidEmail(t *testing.T) {
 	Expect(t, IsValidEmail("test@gmail.com"), true)
 	Expect(t, IsValidEmail("test@gmail.com"), true)
 	Expect(t, IsValidEmail("test@yandex.ru"), true)
 	Expect(t, IsValidEmail("test@yandex.ru"), true)
@@ -68,6 +75,12 @@ func TestIsValidAlias(t *testing.T) {
 	Expect(t, IsValidAlias("/blog/some"), false)
 	Expect(t, IsValidAlias("/blog/some"), false)
 	Expect(t, IsValidAlias("/blog-1"), true)
 	Expect(t, IsValidAlias("/blog-1"), true)
 	Expect(t, IsValidAlias("/blog-some"), true)
 	Expect(t, IsValidAlias("/blog-some"), true)
+
+	Expect(t, IsValidAlias("/shop"), false)
+	Expect(t, IsValidAlias("/shop/"), false)
+	Expect(t, IsValidAlias("/shop/some"), false)
+	Expect(t, IsValidAlias("/shop-1"), true)
+	Expect(t, IsValidAlias("/shop-some"), true)
 }
 }
 
 
 func TestIsValidSingleAlias(t *testing.T) {
 func TestIsValidSingleAlias(t *testing.T) {
@@ -238,6 +251,26 @@ func TestStrToInt(t *testing.T) {
 	Expect(t, StrToInt("string"), 0)
 	Expect(t, StrToInt("string"), 0)
 }
 }
 
 
+func TestFloat64ToStr(t *testing.T) {
+	Expect(t, Float64ToStr(0), "0.00")
+	Expect(t, Float64ToStr(0.5), "0.50")
+	Expect(t, Float64ToStr(15.8100), "15.81")
+}
+
+func TestFloat64ToStrF(t *testing.T) {
+	Expect(t, Float64ToStrF(0, "%.4f"), "0.0000")
+	Expect(t, Float64ToStrF(0.5, "%.4f"), "0.5000")
+	Expect(t, Float64ToStrF(15.8100, "%.4f"), "15.8100")
+}
+
+func TestStrToFloat64(t *testing.T) {
+	Expect(t, StrToFloat64("0.00"), 0.0)
+	Expect(t, StrToFloat64("0.5"), 0.5)
+	Expect(t, StrToFloat64("0.50"), 0.5)
+	Expect(t, StrToFloat64("15.8100"), 15.81)
+	Expect(t, StrToFloat64("15.8155"), 15.8155)
+}
+
 func TestGenerateAlias(t *testing.T) {
 func TestGenerateAlias(t *testing.T) {
 	Expect(t, GenerateAlias(""), "")
 	Expect(t, GenerateAlias(""), "")
 	Expect(t, GenerateAlias("Some page name"), "/some-page-name/")
 	Expect(t, GenerateAlias("Some page name"), "/some-page-name/")

Some files were not shown because too many files changed in this diff