Compare commits

..

61 Commits

Author SHA1 Message Date
Sergey
cab595a36e
Merge pull request #4 from sergeyyarkov/dependabot/npm_and_yarn/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4
2020-07-07 18:40:26 +03:00
dependabot[bot]
13398920bf
Bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-08 06:27:44 +00:00
14777421b6 added markdown 2020-05-17 01:59:55 +03:00
844b4e4dd0 fixed event 2020-05-09 15:13:35 +03:00
cea0057f06 fix json post request 2020-02-13 21:48:09 +03:00
2799b3e7fe categories component fixed 2020-02-13 21:10:11 +03:00
34f4829c63 buyers component fixed 2020-02-13 20:46:27 +03:00
a50caef3ce fixed items component 2020-02-12 22:22:05 +03:00
6a577b8dc4 Merge branch 'master' of https://github.com/sergeyyarkov/digital-store 2020-02-11 20:26:30 +03:00
532b77d7af styles 2020-02-11 20:25:44 +03:00
Sergey
ccb151a00e
Merge pull request #2 from mayerdev/patch-2
Delete package-lock.json
2020-02-11 10:30:01 +03:00
0d9393cb4d Merge branch 'master' of https://github.com/sergeyyarkov/digital-store 2020-02-10 19:44:07 +03:00
fc74b3303c materialize toast 2020-02-10 19:42:44 +03:00
Sergey
da896dd1d3
Merge pull request #3 from mayerdev/patch-3
Update server.js
2020-02-10 19:04:05 +03:00
Sergey
b26777d4f3
Merge pull request #1 from mayerdev/patch-1
Update README.md
2020-02-10 19:00:36 +03:00
Artem Mayer
16ceabe107
Update server.js 2020-02-10 18:56:13 +03:00
Artem Mayer
875963b52c
Delete package-lock.json 2020-02-10 18:50:49 +03:00
Artem Mayer
13acda9581
Update README.md 2020-02-10 18:46:55 +03:00
7f7ce78f83 heroku doc deploy 2020-02-10 18:31:10 +03:00
17f5dc4084 heroku doc deploy 2020-02-10 18:28:25 +03:00
362b0d5dbd heroku doc deploy 2020-02-10 18:26:35 +03:00
0acd466d2d herkou doc deploy 2020-02-10 18:25:54 +03:00
4f6b1f5962 resave session true & readme 2020-02-01 22:34:18 +03:00
bedfca7b18 pkg 2020-02-01 15:13:13 +03:00
ace04d43e4 server file structure 2020-02-01 14:55:34 +03:00
78c102ea94 readme 2020-01-31 22:34:19 +03:00
ef3a9ba19e readme 2020-01-31 22:16:09 +03:00
30d3fe79a5 readme 2020-01-30 23:34:21 +03:00
ada1bdae51 origin header 2020-01-28 19:56:20 +03:00
a79bf89986 readme 2020-01-27 19:08:05 +03:00
51f7549ae8 build 2020-01-26 21:59:42 +03:00
f782b1be05 pagination fixed 2020-01-26 21:59:08 +03:00
2fd54eeae3 readme & disqus 2020-01-25 22:25:49 +03:00
df2ca7b555 disqus comments 2020-01-25 22:14:31 +03:00
bfd2b3f23d readme 2020-01-25 20:31:49 +03:00
e07d879279 added db dump 2020-01-25 20:27:35 +03:00
09df988cba js 2020-01-25 20:12:20 +03:00
5bbe5e68f7 img type & load 2020-01-25 20:11:12 +03:00
b8dc1a55e9 date success 2020-01-25 19:14:08 +03:00
e6a2a54342 log 2020-01-25 19:02:39 +03:00
4da4c1d68c readme 2020-01-25 18:30:40 +03:00
84bb83d63c readme & env 2020-01-25 17:51:53 +03:00
ace07a534b readme 2020-01-25 16:39:31 +03:00
618f044976 readme & session secret 2020-01-25 16:36:02 +03:00
21833480c8 readme 2020-01-25 00:44:10 +03:00
d1f249efb8 def date 2020-01-23 21:00:46 +03:00
76db27811e date ISO str 2020-01-23 20:26:23 +03:00
f33fd4f857 theme site 2020-01-22 23:00:00 +03:00
8a9b907b40 recommend 2020-01-22 21:05:57 +03:00
b7f382dfde css 2020-01-21 21:05:06 +03:00
529d1171bf css 2020-01-21 20:33:58 +03:00
479343048c comments by Commento 2020-01-21 20:20:17 +03:00
20954ef318 admin templates refactor 2020-01-21 19:43:02 +03:00
1ad0e1c2e3 dashboard data info 2020-01-21 19:31:19 +03:00
c95973a05a pug 2020-01-20 20:51:03 +03:00
3fdac50ce8 prealoader orders 2020-01-20 20:47:30 +03:00
b32656e675 front 2020-01-19 21:41:29 +03:00
54f9bc9a61 component orders 2020-01-19 21:25:29 +03:00
bb0e513f1a git settings 2020-01-18 19:32:27 +03:00
d85a1c10b6 readmd 2020-01-18 18:46:02 +03:00
adefcaec06 api def location 2020-01-18 18:30:32 +03:00
58 changed files with 4764 additions and 3067 deletions

9
.babelrc Normal file
View File

@ -0,0 +1,9 @@
{
"presets": [
["@babel/preset-env", {
"targets": [
"last 5 versions"
]
}]
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,3 @@
script(async defer src="https://buttons.github.io/buttons.js") script(async defer src="https://buttons.github.io/buttons.js")
script(src='../js/admin.js') //script(src='../js/admin.js')
script(src="http://localhost:8084/admin.js")

View File

@ -1,3 +1,4 @@
// Script JS // Script JS
script(src='../js/main.js') //script(src='../js/main.js')
script(src='http://localhost:8084/main.js')
script(src="../js/disqus.js") script(src="../js/disqus.js")

View File

@ -19,7 +19,7 @@ main.main-section
p p
strong Контакты strong Контакты
p &nbsp; p &nbsp;
p(style="white-space: pre-wrap;") #{store.contacts} != store.contacts
//- Footer Template //- Footer Template

View File

@ -19,7 +19,7 @@ main.main-section
p p
strong Для того, чтобы купить аккаунты в магазине, нужно проделать следующие шаги: strong Для того, чтобы купить аккаунты в магазине, нужно проделать следующие шаги:
p &nbsp; p &nbsp;
p(style="white-space: pre-wrap;") #{store.howToBuy} != store.howToBuy
//- Footer Template //- Footer Template

View File

@ -16,7 +16,7 @@ main.main-section
.information-title .information-title
h2.small-title Важная информация: h2.small-title Важная информация:
.information-text .information-text
p(style="white-space: pre-wrap;") #{store.info} != store.info
.information-close .information-close
button#information-close.waves-effect.btn.red Закрыть button#information-close.waves-effect.btn.red Закрыть
// Information Block END // Information Block END
@ -95,7 +95,7 @@ main.main-section
.items-info__title.block-title .items-info__title.block-title
i.material-icons info i.material-icons info
h3.med-title Информация h3.med-title Информация
p(style="white-space: pre-wrap;") #{store.infoBottom} != store.infoBottom
// Items Block END // Items Block END
//- Footer Template //- Footer Template

View File

@ -39,15 +39,15 @@ main.main-section
.product-right__title .product-right__title
span Описание: span Описание:
hr hr
p #{description} != description
.product-right__content .product-right__content
h2 Тип аккаунтов #{category} h2 Тип аккаунтов #{category}
p #{type} != type
h2 Формат аккаунтов #{category} h2 Формат аккаунтов #{category}
p #{format} != format
hr hr
h3 Рекомендации к покупке аккаунтов. h3 Рекомендации к покупке аккаунтов.
p #{store.recommendations} != store.recommendations
h3 Отзывы о товаре h3 Отзывы о товаре
#disqus_thread #disqus_thread

View File

@ -1,21 +0,0 @@
module.exports = {
apps : [{
name: 'server',
script: 'server/server.js',
autorestart: true,
watch: true,
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production',
DATABASE_URI: 'mongodb://127.0.0.1:27017',
DATABASE_NAME: '',
EMAIL_LOGIN: '',
EMAIL_PASSWORD: '',
QIWI_SECRET_KEY: ''
}
}],
};

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Webpack output javascript</title>
</head>
<body>
<h1>Webpack output javascript</h1>
</body>
</html>

4824
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
{ {
"name": "digital-store", "name": "digital-store",
"version": "1.1.1", "version": "1.0.0",
"description": "", "description": "",
"main": "server/server.js", "main": "server/server.js",
"scripts": { "scripts": {
"start": "node server/server.js" "start": "node server/server.js",
"build": "webpack --mode production",
"wp-server": "webpack-dev-server --mode development"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +29,7 @@
"express-flash": "0.0.2", "express-flash": "0.0.2",
"express-session": "^1.17.0", "express-session": "^1.17.0",
"log4js": "^6.1.0", "log4js": "^6.1.0",
"marked": "^1.1.0",
"method-override": "^3.0.0", "method-override": "^3.0.0",
"mongodb": "^3.3.4", "mongodb": "^3.3.4",
"multer": "^1.4.2", "multer": "^1.4.2",

View File

@ -1,162 +1,178 @@
const ObjectID = require('mongodb').ObjectID; const ObjectID = require('mongodb').ObjectID
const nodemailer = require('../config/nodemailer.config'); const marked = require('marked')
const nodemailer = require('../config/nodemailer.config')
module.exports = function (server, db) { module.exports = function (server, db) {
server.get('/', async (req, res) => { server.get('/', async (req, res) => {
try { try {
const store = await db.collection('content').findOne({}); const store = await db.collection('content').findOne({})
db.collection('categories').find().toArray((err, result) => { db.collection('categories')
const categories = result.map(category => category.title[0].toUpperCase() + category.title.slice(1)); .find()
res.render('main/index', { .toArray((err, result) => {
pageName: 'index', const categories = result.map(
title: `${store.title} | Главная`, (category) =>
categories, category.title[0].toUpperCase() + category.title.slice(1)
store: { )
title: store.title, res.render('main/index', {
info: store.info, pageName: 'index',
infoBottom: store.infoBottom, title: `${store.title} | Главная`,
color: store.color categories,
} store: {
}); title: store.title,
}); info: marked(store.info),
} catch { infoBottom: marked(store.infoBottom),
res.status(500).render('main/404'); color: store.color,
} },
}); })
server.get('/how-to-buy', async (req, res) => {
try {
const store = await db.collection('content').findOne({});
res.render('main/how-to-buy', {
pageName: 'how-to-buy',
title: `${store.title} | Как купить товар`,
store: {
title: store.title,
howToBuy: store.howToBuy,
color: store.color
}
});
} catch {
res.status(500).render('main/404');
}
});
server.get('/contacts', async (req, res) => {
try {
const store = await db.collection('content').findOne({});
res.render('main/contacts', {
pageName: 'contacts',
title: `${store.title} | Контакты`,
store: {
title: store.title,
contacts: store.contacts,
color: store.color
}
});
} catch {
res.status(500).render('main/404');
}
});
server.get('/comments', async (req, res) => {
try {
const store = await db.collection('content').findOne({});
res.render('main/comments', {
pageName: 'comments',
title: `${store.title} | Отзывы`,
store: {
title: store.title,
color: store.color
}
});
} catch {
res.status(500).render('main/404');
}
});
server.get('/my-orders', async (req, res) => {
try {
const store = await db.collection('content').findOne({});
res.render('main/my-orders', {
pageName: 'my-orders',
title: `${store.title} | Мои покупки`,
store: {
title: store.title,
color: store.color
}
});
} catch {
res.status(500).render('main/404');
}
});
server.get('/product/:id', async (req, res) => {
const store = await db.collection('content').findOne({});
const product = new Promise((resolve, reject) => {
db.collection('items').find({_id: ObjectID(req.params.id)}).toArray()
.then((items) => {
const item = items.find(item => item);
resolve(item);
});
});
product.then((item) => {
return new Promise((resolve, reject) => {
db.collection('categories').find({title: item.category}).toArray()
.then((categories) => {
const category = categories.find(category => category);
resolve({item, category});
});
});
}) })
.then(((data) => { } catch {
const item = data.item, res.status(500).render('main/404')
category = data.category; }
res.render('main/product', { })
title: item.title, server.get('/how-to-buy', async (req, res) => {
image: category.img, try {
count: item.count, const store = await db.collection('content').findOne({})
category: item.category[0].toUpperCase() + item.category.slice(1), res.render('main/how-to-buy', {
price: item.price, pageName: 'how-to-buy',
description: item.description, title: `${store.title} | Как купить товар`,
date: item.date, store: {
id: item._id, title: store.title,
type: category.type, howToBuy: marked(store.howToBuy),
format: category.format, color: store.color,
store: { },
title: store.title, })
recommendations: store.recommendations, } catch {
color: store.color res.status(500).render('main/404')
} }
}); })
})) server.get('/contacts', async (req, res) => {
.catch(() => { try {
res.status(500).render('main/404'); const store = await db.collection('content').findOne({})
}); res.render('main/contacts', {
}); pageName: 'contacts',
server.post('/my-orders', async (req, res) => { title: `${store.title} | Контакты`,
const buyers = await db.collection('buyers').find({email: req.body.email}).toArray(); store: {
if (buyers.length > 0) { title: store.title,
// рендерим html contacts: marked(store.contacts),
const html = buyers.map((buyer) => { color: store.color,
return ` },
})
} catch {
res.status(500).render('main/404')
}
})
server.get('/comments', async (req, res) => {
try {
const store = await db.collection('content').findOne({})
res.render('main/comments', {
pageName: 'comments',
title: `${store.title} | Отзывы`,
store: {
title: store.title,
color: store.color,
},
})
} catch {
res.status(500).render('main/404')
}
})
server.get('/my-orders', async (req, res) => {
try {
const store = await db.collection('content').findOne({})
res.render('main/my-orders', {
pageName: 'my-orders',
title: `${store.title} | Мои покупки`,
store: {
title: store.title,
color: store.color,
},
})
} catch {
res.status(500).render('main/404')
}
})
server.get('/product/:id', async (req, res) => {
const store = await db.collection('content').findOne({})
const product = new Promise((resolve, reject) => {
db.collection('items')
.find({ _id: ObjectID(req.params.id) })
.toArray()
.then((items) => {
const item = items.find((item) => item)
resolve(item)
})
})
product
.then((item) => {
return new Promise((resolve, reject) => {
db.collection('categories')
.find({ title: item.category })
.toArray()
.then((categories) => {
const category = categories.find((category) => category)
resolve({ item, category })
})
})
})
.then((data) => {
const item = data.item,
category = data.category
res.render('main/product', {
title: item.title,
image: category.img,
count: item.count,
category: item.category[0].toUpperCase() + item.category.slice(1),
price: item.price,
description: marked(item.description),
date: item.date,
id: item._id,
type: marked(category.type),
format: marked(category.format),
store: {
title: store.title,
recommendations: marked(store.recommendations),
color: store.color,
},
})
})
.catch(() => {
res.status(500).render('main/404')
})
})
server.post('/my-orders', async (req, res) => {
const buyers = await db
.collection('buyers')
.find({ email: req.body.email })
.toArray()
if (buyers.length > 0) {
// рендерим html
const html = buyers.map((buyer) => {
return `
<b>Номер заказа: ${buyer.bill_id}</b><br> <b>Номер заказа: ${buyer.bill_id}</b><br>
<b>Дата оплаты: ${new Date(buyer.date).toLocaleDateString()}</b><br> <b>Дата оплаты: ${new Date(
buyer.date
).toLocaleDateString()}</b><br>
<b>Данные товара:</b><br> <b>Данные товара:</b><br>
<p>${buyer.data.join('<br>')}</p> <p>${buyer.data.join('<br>')}</p>
<p>======================================================================</p> <p>======================================================================</p>
`; `
}); })
// высылаем данные покупателю // высылаем данные покупателю
try { try {
await nodemailer.transporter.sendMail({ await nodemailer.transporter.sendMail({
from: `"Ваши покупки." <${process.env.EMAIL_LOGIN}>`, from: `"Ваши покупки." <${process.env.EMAIL_LOGIN}>`,
to: req.body.email, to: req.body.email,
subject: "Данные купленных товаров находятся в этом письме.", subject: 'Данные купленных товаров находятся в этом письме.',
html: html.join('') html: html.join(''),
}); })
res.json({status: 'OK'}); res.json({ status: 'OK' })
} catch (error) { } catch (error) {
res.json({status: 'ERR'}); res.json({ status: 'ERR' })
} }
} else { } else {
res.json({status: 'NF'}); res.json({ status: 'NF' })
} }
}); })
} }

View File

@ -0,0 +1,31 @@
import { Component } from '../core/component';
import axios from 'axios';
export class AdministratorComponent extends Component {
constructor(id) {
super(id);
}
setElems() {
this.$el.querySelector('form').addEventListener('submit', this.request.bind(this));
}
async request(e) {
e.preventDefault();
try {
const data = {
id: e.target.elements.id.value,
name: e.target.elements.name.value,
email: e.target.elements.email.value,
oldPass: e.target.elements.oldPass.value,
newPass: e.target.elements.newPass.value
}
const response = await axios.post('/control-panel/administrators/update', data);
alert(response.data);
document.location.reload(true);
} catch (error) {
alert('Невозможно выполнить запрос.');
}
}
}

View File

@ -0,0 +1,53 @@
import { Component } from "../core/component";
import { apiService } from "../services/api.service";
import { renderCategories } from "../templates/categories.template";
import { renderIcons } from "../templates/icons.template";
export class CategoriesComponent extends Component {
constructor(id) {
super(id);
}
setElems() {
// селекты в которых рендерится контент
this.$editSelect = this.$el.querySelector('#editCategory_select'); // редактирование категории
this.$dellSelect = this.$el.querySelector('#deleteCategory_select'); // удаление категории
this.$iconsSelect = this.$el.querySelector('#icons_select'); // выбор иконки
// обработка форм
this.$addForm = this.$el.querySelector('#addCategory');
this.$dellForm = this.$el.querySelector('#deleteCategory');
// добавим ивенты на наши формы
this.$addForm.addEventListener('submit', this.addCategory.bind(this));
this.$dellForm.addEventListener('submit', this.deleteCategory.bind(this));
}
async init() {
const categories = await apiService.getCategories(),
icons = await apiService.getIcons();
this.$editSelect.insertAdjacentHTML('beforeend', renderCategories(categories));
this.$dellSelect.insertAdjacentHTML('beforeend', renderCategories(categories));
this.$iconsSelect.insertAdjacentHTML('beforeend', renderIcons(icons));
M.FormSelect.init(document.querySelectorAll('select'));
}
async addCategory(e) {
e.preventDefault();
const categories = await apiService.getCategories(),
title = this.$addForm.elements[0].value.toLowerCase().trim(),
index = categories.map((category) => {return category.title}).indexOf(title);
index != -1 ? alert('Такая категория уже существует, придумайте другое название.') : this.$addForm.submit();
}
async deleteCategory(e) {
e.preventDefault();
const categories = await apiService.getCategories(),
title = this.$dellForm.elements[0].value.toLowerCase().trim(),
index = categories.map((category) => {return category.title}).indexOf(title);
index != -1 ? this.$dellForm.submit() : alert('Выберите категорию для удаления.');
}
}

View File

@ -0,0 +1,15 @@
import { Component } from '../core/component';
export class ContentComponent extends Component {
constructor(id) {
super(id);
}
setElems() {
this.$info = this.$el.querySelector('#info');
}
init() {
M.textareaAutoResize(this.$info);
}
}

View File

@ -0,0 +1,28 @@
import { Component } from "../core/component";
import { apiService } from "../services/api.service";
import { renderBuyers } from "../templates/buyers.template";
export class DashboardComponent extends Component {
constructor(id, {pagination}, {search}) {
super(id);
this.pagination = pagination;
this.search = search;
}
async init() {
const buyers = await apiService.getBuyers();
this.$table = this.$el.querySelector('table tbody');
this.search.initialize(this.$table, this.getBuyersByBillId);
this.pagination.initialize(buyers.length, this.$table, this.getBuyersByPage);
}
async getBuyersByBillId(bill_id) {
const buyer = await apiService.getBillId(bill_id);
return renderBuyers(buyer);
}
async getBuyersByPage(page) {
const buyers = await apiService.getBuyersOffset(page);
return renderBuyers(buyers);
}
}

View File

@ -0,0 +1,30 @@
import { Component } from "../core/component";
import { apiService } from "../services/api.service";
import { renderIcons } from "../templates/icons.template";
export class IconsComponent extends Component {
constructor(id) {
super(id);
}
setElems() {
this.$icons = this.$el.querySelector('#selectIcons');
this.$delete = this.$el.querySelector('#dellIcon');
this.$delete.addEventListener('submit', this.dellIcon.bind(this));
}
async init() {
const icons = await apiService.getIcons();
this.$icons.insertAdjacentHTML('beforeend', renderIcons(icons));
M.FormSelect.init(this.$icons);
}
async dellIcon(e) {
e.preventDefault();
const icons = await apiService.getIcons(),
currIcon = e.target.elements[0].value,
index = icons.indexOf(currIcon);
index != -1 ? e.target.submit() : alert('Выберите иконку для удаления.');
}
}

View File

@ -0,0 +1,41 @@
import { Component } from "../core/component";
import { apiService } from "../services/api.service";
import { renderItems } from "../templates/items.template";
import { renderCategories } from "../templates/categories.template";
export class ItemsComponent extends Component {
constructor(id, {pagination}, {search}) {
super(id);
this.pagination = pagination;
this.search = search;
}
setElems() {
this.$categories = this.$el.querySelector('#selectCategories');
}
async init() {
const items = await apiService.getItems(),
categories = await apiService.getCategories();
this.$table = this.$el.querySelector('table tbody');
this.search.initialize(this.$table, this.getItemsByText);
this.pagination.initialize(items.length, this.$table, this.getItemsByPage);
this.$categories.insertAdjacentHTML('beforeend', renderCategories(categories));
M.FormSelect.init(this.$categories);
}
async getItemsByText(text) {
const items = await apiService.getItemsQuery(text),
categories = await apiService.getCategories(),
data = await apiService.getItemsData();
return renderItems(items, categories, data);
}
async getItemsByPage(page) {
const items = await apiService.getItemsOffset(page),
categories = await apiService.getCategories(),
data = await apiService.getItemsData();
return renderItems(items, categories, data);
}
}

View File

@ -0,0 +1,7 @@
import { Component } from "../core/component";
export class LoaderComponent extends Component {
constructor(id) {
super(id);
}
}

View File

@ -0,0 +1,135 @@
import { Component } from "../core/component";
export class PaginationComponent extends Component {
constructor(id, {loader}) {
super(id);
this.loader = loader;
}
/**
*
* @param {number} length - кол-во товаров в магазине
* @param {object} body - куда вставляем данные о первой странице
* @param {function} request - функция async запроса
*/
initialize(length, body, request) {
// рендерим кнопку с первой страницой как независимую от других кнопок
this.$el.insertAdjacentHTML('afterbegin', '<li><a href="#" data-move="1">1</a></li><li><span style="position: relative;top: 4px;padding: 4px;">...</span></li>');
// рендерим сами кнопки и кнопки вперед - назад
this.renderBtns(length);
this.renderArrows();
// показ первой страницы
this.move(1, body, request);
// назначаем ивент на кнопки
this.$el.onclick = (e) => this.buttonHandler(e, body, request);
}
// страницу назад - вперед
renderArrows() {
const leftArrowHTML = '<li><a class="disabled" href="#" data-type="prev"><i class="material-icons">chevron_left</i></a></li>',
rightArrowHTML = '<li><a class="disabled" href="#" data-type="next"><i class="material-icons">chevron_right</i></a></li>';
this.$el.insertAdjacentHTML('afterbegin', leftArrowHTML);
this.$el.insertAdjacentHTML('beforeend', rightArrowHTML);
}
renderBtns(length) {
let count = Math.ceil(length / 5),
btnsHTML = '';
// проверяем - если товаров > 21, рендерим максимум до пяти кнопок
length > 21 ? count = 5 : count;
this.count = Math.ceil(length / 5);
for (let i = 2; i <= count; i++) {
btnsHTML += `<li><a href="#" data-move="${i}">${i}</a></li>`;
}
this.$el.insertAdjacentHTML('beforeend', btnsHTML);
}
async move(page, body, request) {
this.loader.show();
const $btns = Array.from(this.$el.querySelectorAll('li a'));
if (page === +$btns[$btns.length - 2].dataset.move && page < this.count) {
const numbers = $btns.filter(btn => btn.dataset.move);
for (let i = 1; i < numbers.length; i++) {
let newIndex = numbers[i].textContent;
++newIndex;
numbers[i].setAttribute('data-move', newIndex);
numbers[i].textContent = newIndex;
}
}
if (page === +$btns[2].dataset.move && page > 2) {
const numbers = $btns.filter(btn => btn.dataset.move);
for (let i = 1; i < numbers.length; i++) {
let newIndex = numbers[i].textContent;
--newIndex;
numbers[i].setAttribute('data-move', newIndex);
numbers[i].textContent = newIndex;
}
}
if (page <= this.count) {
// красим кнопки
const numbers = $btns.filter(btn => btn.dataset.move);
numbers.forEach(btn => btn.parentNode.classList.remove('active'));
numbers.forEach(btn => {
if (page === +btn.dataset.move) {
btn.parentNode.classList.add('active');
}
});
const response = await request(page);
body.innerHTML = '';
if (response != '') {
this.$el.setAttribute('data-page', page);
body.insertAdjacentHTML('afterbegin', response);
} else body.innerHTML = 'Данных нету.';
}
this.loader.hide();
}
buttonHandler(e, body, request) {
e.preventDefault();
if (e.target.dataset.move) {
const page = parseInt(e.target.dataset.move),
target = e.target.parentNode;
this.move(page, body, request);
} else if (e.target.tagName.toLowerCase() === 'i') {
const type = e.target.parentNode.dataset.type,
page = parseInt(this.$el.dataset.page);
switch (type) {
case 'next':
this.next(page, body, request);
break;
case 'prev':
this.prev(page, body, request);
default:
break;
}
}
}
next(page, body, request) {
this.move(++page, body, request);
}
prev(page, body, request) {
--page;
if (page < 1) {
return 0;
} else {
this.move(page, body, request);
}
}
}

View File

@ -0,0 +1,29 @@
import { Component } from "../core/component";
export class SearchComponent extends Component {
constructor(id, {loader}) {
super(id);
this.loader = loader;
}
initialize(insertTo, request) {
this.$el.onchange = (e) => {
this.search(e, insertTo, request);
}
}
async search(e, insertTo, request) {
try {
insertTo.innerHTML = '';
this.loader.show();
const query = e.target.value.trim(),
items = await request(query);
items != '' ? insertTo.insertAdjacentHTML('afterbegin', items) : insertTo.innerHTML = `По запросу "${query}" ничего не найдено...`;
e.target.value = '';
this.loader.hide();
} catch (error) {
console.error('Произошла ошибка.');
}
}
}

View File

@ -0,0 +1,52 @@
const M = require('../frameworks/materialize.min');
export class MaterializeConfig {
constructor() {
document.addEventListener('DOMContentLoaded', function () {
// Closest полифилл для ie11
(function() {
if (!Element.prototype.closest) {
Element.prototype.closest = function(css) {
var node = this;
while (node) {
if (node.matches(css)) return node;
else node = node.parentElement;
}
return null;
};
}
})();
// Matches полифилл для ie11
(function() {
if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.matchesSelector ||
Element.prototype.webkitMatchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector;
}
})()
M.Sidenav.init(document.querySelectorAll('.sidenav'));
M.Collapsible.init(document.querySelectorAll('.collapsible'));
M.Modal.init(document.querySelectorAll('.modal'));
document.querySelector('#type') ? M.textareaAutoResize(document.querySelector('#type')) : false
M.updateTextFields();
// окраска li по нажатию
if (document.querySelector('#currentPage')) {
const currentPage = document.querySelector('#currentPage').dataset.current;
const pages = Array.from(document.querySelectorAll('.collapsible li'));
pages.forEach(function (page) {
if (page.dataset.page) {
if (currentPage === page.dataset.page) {
document.querySelector('#home').classList.remove('active');
page.classList.add('active');
const collapse = M.Collapsible.getInstance(page.closest('.collapsible'));
collapse.open();
}
}
});
}
});
}
}

View File

@ -0,0 +1,25 @@
export class Component {
constructor(id) {
this.$el = document.getElementById(id);
this.$el ? this.setElems() : false;
this.$el ? this.init() : false;
}
init() {}
setElems() {}
onShow() {}
onHide() {}
hide() {
this.$el ? this.$el.classList.add('hide') : false;
this.onHide();
}
show() {
this.$el ? this.$el.classList.remove('hide') : false;
this.onShow();
}
}

31
src/admin/core/modal.js Normal file
View File

@ -0,0 +1,31 @@
export class Modal {
constructor(id, open, close) {
this.$el = document.getElementById(id);
this.open = document.getElementById(open);
this.close = document.getElementById(close);
this.init();
}
init() {
if (this.open) {
this.setElems();
this.open.addEventListener('click', this.show.bind(this));
this.close.addEventListener('click', this.hide.bind(this));
}
}
show(e) {
this.$el.classList.remove('hide');
document.body.style.overflowY = 'hidden';
this.openHandler(e);
}
hide() {
this.$el.classList.add('hide');
document.body.style.overflowY = '';
}
openHandler() {}
setElems() {}
}

File diff suppressed because one or more lines are too long

34
src/admin/index.js Normal file
View File

@ -0,0 +1,34 @@
import { MaterializeConfig } from "./config/materialize.config.js";
import { CategoriesComponent } from "./components/categories.component.js";
import { ItemsComponent } from "./components/items.component.js";
import { LoaderComponent } from "./components/loader.component.js";
import { PaginationComponent } from "./components/pagination.component.js";
import { SearchComponent } from "./components/search.component.js";
import { IconsComponent } from "./components/icons.component.js";
import { ItemEditModal } from "./modals/item-edit.modal.js";
import { ItemDeleteModal } from "./modals/item-delete.modal.js";
import { CategoryEditModal } from "./modals/category-edit.modal";
import { ContentComponent } from "./components/content.component.js";
import { AdministratorComponent } from "./components/administrator.component.js";
import { DashboardComponent } from "./components/dashboard.component.js";
import { BillDataModal } from "./modals/bill-data.modal.js";
// Components
const loader = new LoaderComponent('loader');
const pagination = new PaginationComponent('pagination', {loader});
const search = new SearchComponent('search', {loader});
const categories = new CategoriesComponent('categories');
const icons = new IconsComponent('icons');
const items = new ItemsComponent('items', {pagination}, {search});
const dashboards = new DashboardComponent('dashboard', {pagination}, {search});
const content = new ContentComponent('content');
const administrator = new AdministratorComponent('administrator');
// Modals
const itemEdit = new ItemEditModal('editItemModal', 'edit', 'editItemClose', {loader});
const itemDelete = new ItemDeleteModal('deleteItemModal', 'edit', 'deleteItemClose');
const categoryEdit = new CategoryEditModal('editCategoryModal', 'editCategory_select', 'editCategoryClose', {loader});
const billData = new BillDataModal('billDataModal', 'dashboard', 'billDataClose', {loader});
const materialize = new MaterializeConfig();
console.log('Control-Panel v1.0.0');

View File

@ -0,0 +1,46 @@
import { Modal } from "../core/modal";
import { apiService } from "../services/api.service";
export class BillDataModal extends Modal {
constructor(id, open, close, {loader}) {
super(id, open, close);
this.loader = loader
}
show(e) {
if (e.target.id === 'billDataOpen') {
this.$el.classList.remove('hide');
document.body.style.overflowY = 'hidden';
this.openHandler(e);
}
}
setElems() {
this.$bill_id = this.$el.querySelector('#bill_id');
this.$email = this.$el.querySelector('#email');
this.$method = this.$el.querySelector('#method');
this.$date = this.$el.querySelector('#date');
this.$amount = this.$el.querySelector('#amount');
this.$data = this.$el.querySelector('#data');
}
async openHandler(e) {
try {
this.loader.show();
const buyer = await apiService.getBuyerById(e.target.parentNode.parentNode.dataset.id)
this.$bill_id.value = buyer.bill_id;
this.$email.value = buyer.email;
this.$method.value = buyer.method[0].toUpperCase() + buyer.method.slice(1);
this.$date.value = buyer.date;
this.$amount.value = buyer.amount;
this.$data.textContent = buyer.data.join('\n');
M.textareaAutoResize(this.$data);
M.updateTextFields();
this.loader.hide();
} catch (error) {
alert('Произошла ошибка, обновите страницу')
}
}
}

View File

@ -0,0 +1,60 @@
import { Modal } from "../core/modal";
import { apiService } from "../services/api.service";
import { renderIcons } from "../templates/icons.template";
export class CategoryEditModal extends Modal {
constructor(id, open, close, {loader}) {
super(id, open, close);
this.loader = loader;
}
// перепишем метод для перевода ивента open в событие onchange
init() {
if (this.open) {
this.setElems();
this.open.addEventListener('change', this.show.bind(this));
this.close.addEventListener('click', this.hide.bind(this));
this.$el.querySelector('form').addEventListener('submit', this.updateCategory.bind(this));
}
}
setElems() {
this.$id = this.$el.querySelector('.id');
this.$originalTitle = this.$el.querySelector('.originalTitle');
this.$title = this.$el.querySelector('.title');
this.$type = this.$el.querySelector('.type');
this.$format = this.$el.querySelector('.format');
this.$img = this.$el.querySelector('.img');
}
async openHandler(e) {
try {
this.loader.show();
const icons = await apiService.getIcons(),
category = await apiService.getCategoryById(e.target.value)
this.$img.innerHTML = `<option value='${category.img}' selected>Оставить прежнюю</option>`;
this.$id.value = category._id;
this.$originalTitle.value = category.title[0].toUpperCase() + category.title.slice(1);
this.$title.value = category.title[0].toUpperCase() + category.title.slice(1);
this.$type.value = category.type;
this.$format.value = category.format;
this.$img.insertAdjacentHTML('beforeend', renderIcons(icons));
M.FormSelect.init(this.$img);
M.textareaAutoResize(this.$type);
M.updateTextFields();
this.loader.hide();
} catch (error) {
alert('Произошла ошибка, обновите страницу')
}
}
async updateCategory(e) {
e.preventDefault();
const form = this.$el.querySelector('form');
const categories = await apiService.getCategories();
const id = form.elements[0].value.toLowerCase().trim();
const index = categories.map((category) => {return category._id}).indexOf(id);
index != -1 ? form.submit() : alert('Категории с таким ID не существует.');
}
}

View File

@ -0,0 +1,46 @@
import { Modal } from "../core/modal";
import { apiService } from "../services/api.service";
import axios from "axios";
export class ItemDeleteModal extends Modal {
constructor(id, open, close) {
super(id, open, close);
}
show(e) {
if (e.target.id === 'deleteItem') {
this.$el.classList.remove('hide');
document.body.style.overflowY = 'hidden';
this.openHandler(e);
}
}
setElems() {
this.$content = this.$el.querySelector('.modal-content__form p');
this.$button = this.$el.querySelector('.delete-item');
}
async openHandler(e) {
try {
const id = e.target.parentNode.parentNode.dataset.id,
item = await apiService.getItemById(id);
this.$content.innerHTML = `Вы действительно хотите удалить товар "${item.title}"? Находящиеся в нем аккаунты будут удалены.`;
this.$button.onclick = this.deleteHandler.bind(this, item._id);
} catch (error) {
alert('Произошла ошибка, попробуйте обновить страницу.')
}
}
async deleteHandler(id) {
try {
this.$button.classList.add('hide');
this.$content.innerHTML = '<div class="progress"><div class="indeterminate"></div></div>'
await axios.post('/control-panel/items/delete', {id});
alert('Товар был успешно удален.');
document.location.reload(true);
} catch (error) {
alert('Невозможно выполнить запрос.');
}
}
}

View File

@ -0,0 +1,54 @@
import { Modal } from "../core/modal";
import { apiService } from "../services/api.service";
import { renderCategories } from "../templates/categories.template";
export class ItemEditModal extends Modal {
constructor(id, open, close, {loader}) {
super(id, open, close);
this.loader = loader
}
show(e) {
if (e.target.id === 'editItem') {
this.$el.classList.remove('hide');
document.body.style.overflowY = 'hidden';
this.openHandler(e);
}
}
setElems() {
this.$id = this.$el.querySelector('.id');
this.$title = this.$el.querySelector('.title');
this.$price = this.$el.querySelector('.price');
this.$description = this.$el.querySelector('.description');
this.$category = this.$el.querySelector('.category');
this.$data = this.$el.querySelector('.data');
}
async openHandler(e) {
try {
this.loader.show();
const id = e.target.parentNode.parentNode.dataset.id,
item = await apiService.getItemById(id),
data = await apiService.getDataById(id),
categories = await apiService.getCategories(),
category = await apiService.getCategory(item.category);
item.data = data.data
this.$category.innerHTML = `<option value='${category[0]._id}' selected>Оставить прежнюю</option>`;
this.$id.value = item._id;
this.$title.value = item.title;
this.$price.value = item.price;
this.$description.value = item.description;
this.$category.insertAdjacentHTML('beforeend', renderCategories(categories));
this.$data.value = item.data;
M.FormSelect.init(this.$category);
M.textareaAutoResize(this.$description);
M.textareaAutoResize(this.$data);
M.updateTextFields();
this.loader.hide();
} catch (error) {
alert('Произошла ошибка, обновите страницу')
}
}
}

View File

@ -0,0 +1,160 @@
import axios from "axios";
class ApiService {
constructor(baseUrl) {
this.url = baseUrl;
}
async getCategory(category) {
try {
const request = `${this.url}/api/category/${encodeURIComponent(category)}`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getCategoryById(id) {
try {
const request = `${this.url}/api/category/id/${id}`;
return useRequest(request);
} catch (error) {
console.log(error)
}
}
async getCategories() {
try {
const request = `${this.url}/api/categories`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getItems(sorting) {
try {
if (sorting) {
const request = `${this.url}/api/items?sorting=${sorting}`;
return useRequest(request);
} else {
const request = `${this.url}/api/items`;
return useRequest(request);
}
} catch (err) {
console.error(err);
}
}
async getItemsOffset(page) {
try {
const request = `${this.url}/api/items/page/${page}`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getItemsQuery(text) {
try {
const request = `${this.url}/api/items?q=${text}`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getItemById(id) {
try {
const request = `${this.url}/api/items/id/${id}`;
return useRequest(request);
} catch (error) {
console.log(error)
}
}
async getDataById(id) {
try {
const request = `${this.url}/api/items/data/${id}`;
return useRequest(request);
} catch (error) {
console.log(error)
}
}
async getItemsOne(category, sorting) {
try {
if (sorting) {
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}?sorting=${sorting}`;
return useRequest(request);
} else {
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}`;
return useRequest(request);
}
} catch (err) {
console.error(err);
}
}
async getItemsData() {
try {
const request = `${this.url}/api/items/data`;
return useRequest(request);
} catch (err) {
console.log(err);
}
}
async getBuyers() {
try {
const request = `${this.url}/api/buyers`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getBuyerById(id) {
try {
const request = `${this.url}/api/buyers/${id}`;
return useRequest(request);
} catch (error) {
console.log(error)
}
}
async getBuyersOffset(page) {
try {
const request = `${this.url}/api/buyers/page/${page}`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getBillId(bill_id) {
try {
const request = `${this.url}/api/buyers/${bill_id}`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getIcons() {
try {
const request = `${this.url}/api/icons`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
}
async function useRequest(request) {
const response = await axios.get(request);
return await JSON.parse(response.request.response);
}
export const apiService = new ApiService(document.defaultView.location.origin);

View File

@ -0,0 +1,7 @@
export function renderBuyers(buyers) {
if (!buyers.error) {
return buyers.map(buyer => `<tr data-id="${buyer.bill_id}"><td>${buyer.email}</td><td>${buyer.method[0].toUpperCase() + buyer.method.slice(1)}</td><td>${new Date(buyer.date).toLocaleDateString()}</td><td>${buyer.amount} ₽</td><td><button id="billDataOpen" class="btn orange">Данные аккаунтов</button></td></tr>`).join('');
} else {
return 'Покупателей в магазине пока что нету';
}
}

View File

@ -0,0 +1,3 @@
export function renderCategories(categories) {
return categories.map(category => `<option value='${category._id}' data-icon="../img/service-icons/${category.img}">${category.title[0].toUpperCase() + category.title.slice(1)}</option>`).join('');
}

View File

@ -0,0 +1,3 @@
export function renderIcons(icons) {
return icons.map(icon => `<option value='${icon}' data-icon="../img/service-icons/${icon}">${icon}</option>`).join('');
}

View File

@ -0,0 +1,13 @@
export function renderItems(items, categories, info) {
if (!items.error) {
return categories.map(category => {
return items.map((item, i) => {
if (item.category.toLowerCase() === category.title.toLowerCase()) {
return `<tr data-id="${item._id}"><td><img src="../img/service-icons/${category.img}" alt="alt"></td><td>${item.title}</td><td>${item.category[0].toUpperCase() + item.category.slice(1)}</td><td>${new Date(item.date).toLocaleDateString()}</td><td>${item.count}</td><td>${item.price} ₽</td><td style="display: flex"><button id="deleteItem" class="btn red">Удалить</button><button id="editItem" class="btn orange">Изменить</button></td></tr>`;
}
}).join('');
}).join('');
} else {
return 'Данных нету.';
}
}

View File

@ -0,0 +1,22 @@
import { Component } from "../core/component";
export class BreadcrumbComponent extends Component {
constructor(id) {
super(id);
}
init() {
if(this.$el) {
const btn = this.$el.querySelector('#breadcrumbBtn');
btn.addEventListener('click', buttonHandler);
}
}
}
function buttonHandler(e) {
e.preventDefault();
if (e.target.dataset.category) {
localStorage.setItem('category', e.target.dataset.category.toLowerCase());
window.location.href = '/';
}
}

View File

@ -0,0 +1,81 @@
import { Component } from "../core/component";
import { apiService } from "../services/api.service";
import { renderItems } from "../templates/items.template";
export class CategoryComponent extends Component {
constructor(id, {items}, {filter}) {
super(id);
this.items = items;
this.filter = filter;
}
init() {
if (this.$el) {
this.$el.addEventListener('click', buttonHandler.bind(this));
if (localStorage.getItem('category')) {
localStorage.removeItem('sortByAll', 'true'); // установка значения для компонента Filter
const category = localStorage.getItem('category');
breadcrumbActiveLink.call(this, category); // окрашиваем категорию по нажатию на breadcrumb
showItems.call(this, category); // отображем айтемсы если был нажат breadcrumb
localStorage.removeItem('category'); // очищаем ключ category чтобы данные не показывались всегда
}
}
}
}
async function showItems(category) {
const targetCategory = await apiService.getCategory(category);
const fData = await apiService.getItemsOne(category),
html = renderItems(targetCategory, fData);
this.items.onHide();
this.items.insertItems(html);
}
async function buttonHandler(e) {
e.preventDefault();
if (e.target.dataset.category && e.target.dataset.category != 'all') { // Выполняем логику если нажали на кнопки кроме "Все товары"
localStorage.removeItem('sortByAll'); // установка значения для компонента Filter
this.filter.clearChecked();
this.items.onShow();
btnActiveLink(e);
const category = await apiService.getCategory(e.target.dataset.category),
fData = await apiService.getItemsOne(e.target.dataset.category),
html = renderItems(category, fData);
this.items.onHide();
this.items.insertItems(html);
} else if (e.target.dataset.category === 'all') { // Выполняем логику если нажали на кнопку "Все товары"
localStorage.setItem('sortByAll', 'true'); // установка значения для компонента Filter
this.filter.clearChecked();
this.items.onShow();
btnActiveLink(e);
const fData = await apiService.getItems();
const categories = await apiService.getCategories();
const html = renderItems(categories, fData);
this.items.onHide();
this.items.insertItems(html);
}
}
// Окраска li по нажатию
function btnActiveLink(e) {
Array.from(e.target.parentNode.parentNode.querySelectorAll('#catLi')).forEach(li => li.classList.remove('active'));
e.target.classList.add('active');
localStorage.setItem('currentCategory', e.target.dataset.category);
}
// Окраска li по нажатию на breadcrumb
function breadcrumbActiveLink(category) {
const links = this.$el.querySelectorAll('#catLi');
Array.from(links).forEach(li => li.classList.remove('active'));
Array.from(links).forEach(li => li.dataset.category === category ? li.classList.add('active') : false);
Array.from(links).forEach(li => li.dataset.category === category ? localStorage.setItem('currentCategory', li.dataset.category) : false);
}

View File

@ -0,0 +1,56 @@
import { Component } from "../core/component";
import { apiService } from "../services/api.service";
import { renderItems } from "../templates/items.template";
export class FilterComponent extends Component {
constructor(id, {items}) {
super(id);
this.items = items;
}
init() {
if (this.$el) {
this.$el.addEventListener('click', sortByAllHandler.bind(this));
this.$el.addEventListener('click', sortByOneHandler.bind(this));
}
}
clearChecked() {
if (this.$el) {
const options = this.$el.querySelectorAll('.filter-options form input');
Array.from(options).forEach(option => option.checked = false);
}
}
}
async function sortByAllHandler(e) {
if (localStorage.getItem('sortByAll')) {
if (e.target.id) {
const sorting = e.target.id;
this.items.onShow();
const fData = await apiService.getItems(sorting);
const categories = await apiService.getCategories();
const html = renderItems(categories, fData);
this.items.onHide();
this.items.insertItems(html);
}
}
}
async function sortByOneHandler(e) {
if (!localStorage.getItem('sortByAll')) {
if (e.target.id) {
const sorting = e.target.id;
this.items.onShow();
const category = await apiService.getCategory(localStorage.getItem('currentCategory')),
fData = await apiService.getItemsOne(localStorage.getItem('currentCategory'), sorting),
html = renderItems(category, fData);
this.items.onHide();
this.items.insertItems(html);
}
}
}

View File

@ -0,0 +1,18 @@
import { Component } from "../core/component";
import { insertCount } from "../other/counter";
export class HeaderComponent extends Component {
constructor(id) {
super(id);
}
init() {
if (this.$el) {
const currentPage = this.$el.querySelector('#desktop-nav');
const pages = Array.from(this.$el.querySelectorAll('#page'));
pages.forEach(li => li.dataset.page === currentPage.dataset.current ? li.classList.add('active') : false);
// ф-я счетика товаров
localStorage.getItem('cart') ? insertCount('counter', JSON.parse(localStorage.getItem('cart'))) : false;
}
}
}

View File

@ -0,0 +1,22 @@
import { Component } from "../core/component";
export class InformationComponent extends Component {
constructor(id) {
super(id);
}
init() {
if (this.$el) {
if (!localStorage.getItem('visited')) {
this.show();
}
const btn = this.$el.querySelector('#information-close');
btn.addEventListener('click', buttonHandler.bind(this));
}
}
}
function buttonHandler() {
localStorage.setItem('visited', JSON.stringify(true));
this.hide();
}

View File

@ -0,0 +1,119 @@
import { Component } from '../core/component'
import { apiService } from '../services/api.service'
import { insertCount } from '../other/counter'
import { renderItems } from '../templates/items.template'
export class ItemsComponent extends Component {
constructor(id, { loader }, { notification }) {
super(id)
this.loader = loader
this.notification = notification
this.$el ? (this.roster = this.$el.querySelector('#roster')) : false
if (localStorage.getItem('cart')) {
this.cart = JSON.parse(localStorage.getItem('cart'))
} else {
this.cart = []
}
}
async init() {
if (this.$el) {
localStorage.setItem('sortByAll', 'true') // установка значения для компонента Filter
this.$el.addEventListener('click', addItem.bind(this))
if (!localStorage.getItem('category')) {
// Получаем все товары и выводим их если нету ключа category в ls
const fData = await apiService.getItems()
const categories = await apiService.getCategories()
const html = renderItems(categories, fData)
this.insertItems(html)
this.btnColor()
this.loader.hide()
}
}
}
onHide() {
this.$el.querySelector('#roster').innerHTML = ''
this.loader.hide()
}
onShow() {
this.$el.querySelector('#roster').innerHTML = ''
this.loader.show()
}
btnColor() {
if (localStorage.getItem('cart')) {
// Окраска кнопок айтемов которые находятся в корзине
const obj = JSON.parse(localStorage.getItem('cart'))
obj.forEach((item) => {
if (
this.$el.querySelector(
`#rosterButtons[data-id="${item.id}"] #addItem`
)
) {
this.$el
.querySelector(`#rosterButtons[data-id="${item.id}"] #addItem`)
.classList.add('orange')
this.$el.querySelector(
`#rosterButtons[data-id="${item.id}"] #addItem i`
).innerHTML = 'close'
}
})
}
}
insertItems(html) {
this.roster.insertAdjacentHTML('afterbegin', html)
this.btnColor()
}
}
function addItem(e) {
if (e.target.localName === 'button' && e.target.id === 'addItem') {
const target = e.target
const item = {
id: e.target.parentNode.dataset.id,
title: e.target.parentNode.dataset.title,
count: e.target.parentNode.dataset.count,
price: e.target.parentNode.dataset.price,
}
renderObj(item, this, target)
}
}
function renderObj(obj, context, target) {
if (context.cart.length > 0) {
let insterted = false
let index
// Проверка на существующий айтем в корзине
context.cart.forEach((item, i) => {
if (item.id === obj.id) {
insterted = true
index = i
}
})
if (insterted === false) {
context.cart.push(obj)
localStorage.setItem('cart', JSON.stringify(context.cart))
target.classList.add('orange')
target.querySelector('i').innerHTML = 'close'
insertCount('counter', context.cart)
window.M.toast({ html: 'Товар был успешно добавлен в корзину.' })
} else {
context.cart.splice(index, 1)
localStorage.setItem('cart', JSON.stringify(context.cart))
target.classList.remove('orange')
target.querySelector('i').innerHTML = 'shopping_cart'
insertCount('counter', context.cart)
}
} else {
context.cart.push(obj)
localStorage.setItem('cart', JSON.stringify(context.cart))
target.classList.add('orange')
target.querySelector('i').innerHTML = 'close'
insertCount('counter', context.cart)
window.M.toast({ html: 'Товар был успешно добавлен в корзину.' })
}
}

View File

@ -0,0 +1,7 @@
import { Component } from "../core/component";
export class LoaderComponent extends Component {
constructor(id) {
super(id);
}
}

View File

@ -0,0 +1,50 @@
import { Component } from "../core/component";
export class NotificationComponent extends Component {
constructor(id) {
super(id);
}
notificate(text, color, delay) {
const card = document.createElement('div');
card.classList.add('card-panel');
card.style.backgroundColor = color;
card.innerHTML = `<span class="white-text">${text}</span>`;
this.$el.innerHTML += card.outerHTML;
this.removeNotificate(delay);
}
removeNotificate(delay) {
// Полифилл для IE ^9
(function() {
var arr = [window.Element, window.CharacterData, window.DocumentType];
var args = [];
arr.forEach(function (item) {
if (item) {
args.push(item.prototype);
}
});
(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty('remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
this.parentNode.removeChild(this);
}
});
});
})(args);
})();
setTimeout(() => {
this.$el.querySelector('.card-panel').remove();
}, delay || 2000);
}
}

View File

@ -0,0 +1,43 @@
import { Component } from "../core/component";
import axios from "axios";
export class OrdersComponent extends Component {
constructor(id, {notification}, {loader}) {
super(id);
this.notification = notification;
this.loader = loader;
}
init() {
if (this.$el) {
let valid = true;
this.$el.querySelector('form').onsubmit = async (e) => {
e.preventDefault();
if (valid) {
valid = false;
this.loader.show();
const email = e.target.elements.email.value,
request = await axios.post('/my-orders', {email});
switch (request.data.status) {
case 'OK':
window.M.toast({html: `Данные были успешно высланы на почту "${email}".`})
break;
case 'NF':
window.M.toast({html: `Покупатель с почтой "${email}" не был найден.`})
break;
case 'ERR':
window.M.toast({html: `Произошла ошибка, попробуйте повторить запрос позже.`})
break;
default:
break;
}
this.loader.hide();
setTimeout(() => valid = true, 10000);
} else {
window.M.toast({html: `Подождите еще нескольно секунд прежде чем сделать запрос...`})
}
};
}
}
}

View File

@ -0,0 +1,9 @@
const M = require('../frameworks/materialize.min');
export class MaterializeConfig {
constructor() {
document.addEventListener('DOMContentLoaded', function () {
M.Sidenav.init(document.querySelectorAll('.sidenav'))
M.FormSelect.init(document.querySelectorAll('select'))
});
}
}

View File

@ -0,0 +1,20 @@
export class Component {
constructor(id) {
this.$el = document.getElementById(id);
this.init();
}
init() {}
onShow() {}
onHide() {}
hide() {
this.$el ? this.$el.classList.add('hide') : false
}
show() {
this.$el ? this.$el.classList.remove('hide') : false
}
}

33
src/main/core/modal.js Normal file
View File

@ -0,0 +1,33 @@
export class Modal {
constructor(id, open, close) {
this.$el = document.getElementById(id);
this.open = document.getElementById(open);
this.close = document.getElementById(close);
this.init();
}
init() {
if (this.open != null) {
this.open.addEventListener('click', openHandler.bind(this));
this.close.addEventListener('click', closeHandler.bind(this));
}
}
show() {
this.$el.classList.remove('hide');
document.body.style.overflowY = 'hidden';
}
hide() {
this.$el.classList.add('hide');
document.body.style.overflowY = '';
}
}
function openHandler() {
this.show();
}
function closeHandler() {
this.hide();
}

File diff suppressed because one or more lines are too long

30
src/main/index.js Normal file
View File

@ -0,0 +1,30 @@
import { HeaderComponent } from "./components/header.component.js";
import { InformationComponent } from "./components/information.component.js";
import { LoaderComponent } from "./components/loader.component.js";
import { ItemsComponent } from "./components/items.component.js";
import { CategoryComponent } from "./components/category.component.js";
import { BreadcrumbComponent } from "./components/breadcrumb.component.js";
import { FilterComponent } from "./components/filter.component.js";
import { CartModal } from "./modals/cart.modal.js";
import { PayModal } from "./modals/pay.modal.js";
import { NotificationComponent } from "./components/notification.component.js";
import { MaterializeConfig } from "./config/materialize.config.js";
import { OrdersComponent } from "./components/orders.component.js";
// Components
const header = new HeaderComponent('header');
const loader = new LoaderComponent('loader');
const information = new InformationComponent('information');
const notification = new NotificationComponent('notification');
const items = new ItemsComponent('items', {loader}, {notification});
const filter = new FilterComponent('filter', {items});
const category = new CategoryComponent('category', {items}, {filter});
const breadcumb = new BreadcrumbComponent('breadcrumb');
const orders = new OrdersComponent('orders', {notification}, {loader});
// Modals
const cart = new CartModal('cart', 'cartOpen', 'cartClose', {items});
const pay = new PayModal('pay', 'pay-js', 'payClose');
const materialize = new MaterializeConfig();
console.log('Main Page');

View File

@ -0,0 +1,96 @@
import { Modal } from "../core/modal";
import { insertCount } from "../other/counter";
import axios from "axios";
export class CartModal extends Modal {
constructor(id, open, close, {items}) {
super(id, open, close);
this.items = items;
if (this.$el) {
this.open.addEventListener('click', this.openHandler.bind(this));
this.close.addEventListener('click', this.closeHandler.bind(this));
this.list = this.$el.querySelector('#modalList');
}
}
closeHandler() {
this.removeItem = null;
this.makePay = null;
}
openHandler() {
this.removeItem != null ? this.list.addEventListener('click', this.removeItem.bind(this)) : false
this.makePay != null ? this.$el.querySelector('form').addEventListener('submit', this.makePay.bind(this)) : false;
this.list.innerHTML = '';
this.insertItems(this.list);
}
insertItems(list) {
if (localStorage.getItem('cart')) {
const items = JSON.parse(localStorage.getItem('cart'));
if (items.length == 0) {
list.innerHTML = '<p style="text-align:center;padding:15px;">Вы не добавили ни один товар</p>';
this.$el.querySelector('form').classList.add('hide');
} else {
this.$el.querySelector('form').classList.remove('hide');
}
items.forEach(item => this.list.innerHTML += `<div data-id="${item.id}" data-title="${item.title}" data-price="${item.price}" data-count="1" class="modal-content__item"><div class="modal-content__title"><a href="/product/${item.id}"><b>${item.title}</b></a><br><span class="in-stock">В наличии: ${item.count} (шт)</span></div><div class="modal-content__right"><span id="price">${parseFloat(item.price).toFixed(2)}₽</span></div><div class="modal-content__remove"><span><i data-id="${item.id}" class="material-icons" id="removeItem">close</i></span></div></div>`);
insertCount('modalCounter', this.items.cart);
insertCount('counter', this.items.cart);
this.insertTotalPrice('totalPrice', this.items.cart);
} else {
list.innerHTML = '<p style="text-align:center;padding:15px;">Вы не добавили ни один товар</p>';
}
}
removeItem(e) {
if (e.target.id === 'removeItem') {
const id = e.target.dataset.id;
let index;
this.items.cart.forEach((item, i) => item.id === id ? index = i : false);
this.items.cart.splice(index, 1);
localStorage.setItem('cart', JSON.stringify(this.items.cart));
this.list.innerHTML = '';
this.insertItems(this.list);
this.defaultBtnColor(id);
}
}
insertTotalPrice(id, array) {
let totalPrice = 0;
array.forEach(item => totalPrice += parseFloat(item.price));
this.$el.querySelector(`#${id}`).innerHTML = `${totalPrice.toFixed(2)}₽`;
}
async makePay(e) {
e.preventDefault();
const payInfo = {items: this.items.cart.map(item => {
return {
id: item.id,
title: item.title,
price: parseFloat(item.price)
}
})};
let totalPrice = 0;
payInfo.items.forEach(item => totalPrice += parseFloat(item.price));
payInfo.totalPrice = Math.round(totalPrice * 100) / 100;
payInfo.payMethod = this.$el.querySelector('.payment-field input').value;
payInfo.email = this.$el.querySelector('.email-field input').value;
const response = await axios.put('/payment', payInfo);
response.data.payUrl ? document.location.href = response.data.payUrl : alert(response.data);
}
defaultBtnColor(id) {
if (this.items.roster) {
if (this.items.roster.querySelector(`#rosterButtons[data-id="${id}"] #addItem`)) {
this.items.roster.querySelector(`#rosterButtons[data-id="${id}"] #addItem`).classList.remove('orange');
this.items.roster.querySelector(`#rosterButtons[data-id="${id}"] #addItem i`).innerHTML = 'shopping_cart';
}
}
}
}

View File

@ -0,0 +1,47 @@
import { Modal } from "../core/modal";
import axios from "axios";
export class PayModal extends Modal {
constructor (id, open, close) {
super(id, open, close);
if (this.open != null) {
this.open.addEventListener('click', this.openHandler.bind(this));
this.close.addEventListener('click', this.closeHandler.bind(this));
}
}
closeHandler() {
this.$el.querySelector('form').onsubmit = null;
}
openHandler(e) {
if (e.target.id === 'payOpen') {
const targetInfo = e.target.parentNode.dataset;
this.$el.querySelector('.title-field input').value = targetInfo.title;
this.show();
this.$el.querySelector('form').onsubmit = (e) => {
this.makePay(e, targetInfo);
}
} else {
this.hide();
}
}
async makePay(e, info) {
e.preventDefault();
const payInfo = {
items: [{
id: info.id,
title: info.title
}],
totalPrice: parseFloat(info.price)
}
payInfo.payMethod = this.$el.querySelector('.payment-field input').value;
payInfo.email = this.$el.querySelector('.email-field input').value;
const response = await axios.put('/payment', payInfo);
response.data.payUrl ? document.location.href = response.data.payUrl : alert(response.data);
}
}

View File

@ -0,0 +1,4 @@
export function insertCount(id, array) {
const counter = document.getElementById(id);
counter.innerHTML = array.length;
}

View File

@ -0,0 +1,61 @@
import axios from 'axios';
class ApiService {
constructor(baseUrl) {
this.url = baseUrl;
}
async getCategory(category) {
try {
const request = `${this.url}/api/category/${encodeURIComponent(category)}`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getCategories() {
try {
const request = `${this.url}/api/categories`;
return useRequest(request);
} catch (err) {
console.error(err);
}
}
async getItems(sorting) {
try {
if (sorting) {
const request = `${this.url}/api/items?sorting=${sorting}`;
return useRequest(request);
} else {
const request = `${this.url}/api/items`;
return useRequest(request);
}
} catch (err) {
console.error(err);
}
}
async getItemsOne(category, sorting) {
try {
if (sorting) {
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}?sorting=${sorting}`;
return useRequest(request);
} else {
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}`;
return useRequest(request);
}
} catch (err) {
console.error(err);
}
}
}
async function useRequest(request) {
const response = await axios.get(request);
return await JSON.parse(response.request.response);
}
export const apiService = new ApiService(document.defaultView.location.origin);

View File

@ -0,0 +1,64 @@
export function renderItems(categories, items) {
let html = '';
try {
categories.forEach(category => {
html += `<div class="items-category">
<h2 class="small-title">${category.title}</h2>
</div>`;
category.itemsInCategory = 0; // счетчит для отслеживания категорий где нету айтемов
try {
items.forEach(item => {
if (item.category.toLowerCase() === category.title.toLowerCase()) {
category.itemsInCategory += 1;
html += `
<div class="items-list">
<div class="items-list__name">
<div class="list-icon">
<img src="./img/service-icons/${category.img}" alt="#">
</div>
<div class="list-title">
<a href="/product/${item._id}">${item.title}</a>
</div>
</div>
<div class="items-list__info">
<div class="list-date">
<small>${diffDates(new Date(), new Date(item.date))}</small>
</div>
<div class="list-count">
<span>${item.count} шт.</span>
</div>
<div class="list-one">
<span>Цена за 1 шт.</span>
</div>
<div class="list-price">
<span>${item.price.toFixed(2)}₽</span>
</div>
<div id="rosterButtons" data-id="${item._id}" data-title="${item.title}" data-count="${item.count}" data-price="${item.price} class="list-buttons">
<button id="payOpen" class="btn-small waves-effect waves-light">Купить</button>
<button id="addItem" class="btn-small waves-effect waves-light"><i
class="material-icons">shopping_cart</i></button>
</div>
</div>
</div>`;
}
})
category.itemsInCategory === 0
? html += '<div class="items-list"><span>В этой категории товаров пока что нету.</span></div>'
: false;
} catch {
html += '<div class="items-list"><span>В этой категории товаров пока что нету.</span></div>';
}
});
} catch {
html += '<div class="items-list"><span>Добавьте хотя бы одну категорию.</span></div>';
}
return html;
}
function diffDates(firstDate, secondDate) {
let diff = Math.floor((firstDate - secondDate) / (60 * 60 * 24 * 1000));
if (diff <= 0) {
return 'добавлено сегодня';
}
return diff + ' дней назад';
}

26
webpack.config.js Normal file
View File

@ -0,0 +1,26 @@
const path = require('path');
module.exports = {
entry: {
admin: ['@babel/polyfill', './src/admin/index.js'],
main: ['@babel/polyfill', './src/main/index.js']
},
output: {
filename: '[name].js',
path: path.join(__dirname, '/dist/public/js/')
},
resolve: {
extensions: ['.js']
},
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }
]
},
devServer: {
contentBase: './dist/public/js',
proxy: {
"/": 'http://localhost:3000'
},
}
}