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 4353 additions and 3277 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules node_modules
package-lock.json
.env .env

492
README.md
View File

@ -1,16 +1,478 @@
# Панель управления: <p align="center">
Для входа в панель управления магазином используйте данные: <a href="http://materializecss.com/">
```js <img src="https://svgshare.com/i/HWW.svg" width="150">
Login: admin@support.ru </a>
Password: admin </p>
<h3 align="center">Digital Store</h3>
<p align="center">
Digital Store - это настраиваемый интернет-магазин на платформе NodeJS для продажи ваших цифровых товаров.<br>
<a href="https://polar-peak-95205.herokuapp.com/">Демонстрация функционирования интернет-магазина.</a>
<br>
<br>
<img alt="GitHub package.json version (branch)" src="https://img.shields.io/github/package-json/v/sergeyyarkov/digital-store/production?color=%2326a69a">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/sergeyyarkov/digital-store">
<img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/sergeyyarkov/digital-store/total">
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/sergeyyarkov/digital-store?color=%23f9a825">
<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/sergeyyarkov/digital-store"><br>
<a href="https://david-dm.org/sergeyyarkov/digital-store">
<img src="https://david-dm.org/sergeyyarkov/digital-store/status.svg" alt="dependencies Status badge">
</a>
<a href="https://david-dm.org/sergeyyarkov/digital-store#info=devDependencies">
<img src="https://david-dm.org/sergeyyarkov/digital-store/dev-status.svg" alt="devDependency Status badge">
</a>
<a href="https://github.com/sergeyyarkov/digital-store/blob/master/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/sergeyyarkov/digital-store">
</a>
</p>
## Содержание
- [О проекте](#О-проекте)
- [Структура репозитория](#Структура-репозитория)
- [Установка и настройка скрипта](#Установка-и-настройка-скрипта)
- [Запуск скрипта на локальном компьютере](#Запуск-скрипта-на-локальном-компьютере)
- [Запуск скрипта на удаленном сервере](#Запуск-скрипта-на-удаленном-сервере)
- [Выгрузка скрипта на бесплтаный хостинг от Heroku](#Выгрузка-скрипта-на-бесплтаный-хостинг-от-Heroku)
- [Переменные среды](#Переменные-среды)
- [API](#API)
- [Требования](#Требования)
- [Лицензия](#Лицензия)
## О проекте:
Этот проект представляет из себя интернет-магазин на платформе NodeJS, Вы можете продавать свои цифровые товары именно загрузив этот репозиторий, в [панели управления](#Панель-управления) Вы сможете создавать, удалять и редактировать товары или категории . В проекте используется концепция REST API, поэтому для отдачи информации клиенту существует отдельный API. Информация о покупателях, товарах, категориях и т.д хранится в БД MongoDB (используется нативный драйвер).
### Функционал покупателя:
- Возможность выбирать интересующие категории, переход по одиночным страницам конкретного товара.
- Возможность комментировать как отдельный товар, так и весь магазин на отдельной странице с помощью системы комментариев Disqus.
- Возможность фильтровать интересующую категорию или все товары через фильтр.
- Покупатель может совершать одну или несколько покупок через корзину или кнопку "Купить".
- Оплата осуществляется через сервис QIWI API.
- Результатом оплаты будут данные одного или нескольких товаров, все данные отсылаются на электронную почту покупателя, которую он указал при создании заказа.
- Покупатель может узнать все свои совершенные покупки в магазине на отдельной странице.
### Функционал администратора:
Для входа в админ-панель необходимо дописать в адресной строке `/admin`, после Вы попадете на страницу авторизации где нужно ввести данные в формате `email:password`. По умолчанию `admin@support.ru:admin`
- Пароль админа хэшируется через `bcrypt` для безопасности и записывается в БД.
- На главной странице есть небольшая статистика, где можно посмотреть: сумму проданных товаров, кол-во проданных товаров, кол-во товаров в наличии.
- На главной странце так же можно узнать подробные данные о покупателях: номер заказа, почта покупателя, способ оплаты, дата покупки, сумма и данные которые были высланы по почте.
- Возможность находить конкретного покупателя через поле (нужно знать номер заказа).
- Возможность добавлять, удалять или редактировать товары на странице "Товары", так же можно посмотреть и информацию о товаре.
- Поиск товаров через поле поиска (нужно знать частичное или полное название товара).
- Добавление, удаление и редактирование категорий через страницу "Категории", каждой категории присваивается собственная иконка, которую можно загрузить через страницу "Иконки".
- Возможность править контент, изменять общее оформление магазина (только цвет).
- Возможность изменять данные администратора: почта, пароль, имя.
## Структура репозитория:
В основе всего репозитория существуют две ветки. Ветка master предназначена для разработки приложения, для выгрузки готового приложения на сервер используйте ветку production.
## Установка и настройка скрипта:
### Запуск скрипта на локальном компьютере:
- [Установите последний релиз Digital Store](https://github.com/sergeyyarkov/digital-store/releases) из GitHub репозитория.
- Или клонировать репозиторий `git clone https://github.com/sergeyyarkov/digital-store.git -b production`.
- Установите [Node.js](https://nodejs.org/ru/) не ниже версии 7.6.0, в процессе установки Node.js установите Node Package Manager.
- Установите БД MongoDB на ваш компьютер или создайте кластер на сайте [www.mongodb.com](https://www.mongodb.com/) и импортируйте туда дамп структуры БД, которые находится в корне приложения `/database_dump`.
- Для работоспособности интернет-магазина необходимо установить зависимости. Выполните комманду `npm i`.
- Далле необходимо настроить переменные среды. Создайте файл `.env` в корне вашего интернет-магазина и запишите туда [значения для переменных](#Переменные-среды). Обратите внимание, что для выгрузки скрипта на production сервер, нужно устанавливать переменные среды для вашего сервера, файл `.env` создавать не нужно.
- После этого, введите `npm run start` и перейдите по адресу `localhost:3000`, вы увидите уже рабочий интернет-магазин который запущен у вас на локальном компьютере.
### Запуск скрипта на удаленном сервере:
В данном примере на удаленном сервере будет использоваться связка `Ubuntu 18.04, Nginx, MongoDB, PM2, NodeJS` + `SSL Lets Encrypt`
- Переходим на сайт [RUVDS.COM](https://ruvds.com/pr4435) и приобретаем услугу VPS-хостинга.
- Подключаемся к серверу по SSH и следуем далее описанным шагам.
- Устанавливаем NodeJS:
```bash
apt-get update && apt-get install curl -y
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
source ~/.bashrc
nvm install 12
``` ```
# Переменные среды: Если при выполнении команд `node -v` и `npm -v` выдается версия, то идем дальше.
Создайте файл .env в корне приложения и запишите переменные: - Создаем папку `/apps`, после переходим в нее и клонируем проект с github и переходим в наш магазин:
```js
SESSION_SECRET=secret ```bash
DATABASE_URI=подключение к БД MONGODB mkdir apps
DATABASE_NAME=имя БД
QIWI_SECRET_KEY=секретный ключ QIWI API cd apps/
EMAIL_LOGIN=логин от почты
EMAIL_PASSWORD=пароль от почты git clone https://github.com/sergeyyarkov/digital-store.git -b production
```
cd digital-store/
```
- Устанавливаем зависимости для интернет-магазина `npm install`.
- Устанавливаем MongoDB:
```bash
apt install -y mongodb
systemctl status mongodb <--- проверяем состояние службы, должно быть active (running)
```
Теперь нужно дать доступ для подключения к БД именно с вашего компьютера, это нам понадобится для импортирования дампа БД:
ip адресс берете с сайта 2ip
```bash
ufw enable
ufw allow from ваш_ip_адресс_компьютера/32 to any port 27017
```
- Далее открываем конфигурационный файл MongoDB: `nano /etc/mongodb.conf`:
Добавляем IP-адрес вашего сервера в значение bindIP:
```bash
logappend=true
bind_ip = 127.0.0.1,your_server_ip
#port = 27017
```
- Сохраните файл, закройте редактор и перезапустите MongoDB: `systemctl restart mongodb`:
- Теперь доступ к БД есть только у вашего компьютера, далее скачиваем [Studio 3T](https://studio3t.com/) и создаем новое соединение, ip адресс указываем вашего сервера.
- После подключения, создаем новую БД и называем её как хотим.
- Кликаем по вашей созданной БД и вверху нажимаем `Import`, после выбираем все JSON файлы, которые хранятся в папке `/database_dump`.
- Если все 6 коллекций успешно импортировались, то идем дальше.
- Устанавливаем менеджер процессов PM2 `npm install -g pm2`.
- Редактируем файл с переменными в файле ecosystem.config.js и устанавливаем свои значения `nano ecosystem.config.js`.
- Далее запускаем наше приложение:
```bash
pm2 start ecosystem.config.js --env production <--- стартуем наше приложение
pm2 status <--- как узнать статус запущенных процессов
pm2 save <--- сохраняем процесс
pm2 startup ubuntu <--- автоматически запускаем наше приложение в случае рестарта сервера
```
- Настраиваем файрволл:
```bash
ufw allow ssh
ufw allow http
ufw allow https
```
- Далее устанавливаем веб-сервер nginx `apt install nginx`, после редактируем конфигурационный файл `nano /etc/nginx/sites-available/default`:
```bash
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:3000; #порт запущенного приложения
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
```
- Обязательно настройте gzip сжатие, ваш сайт будет работать быстрее:
```bash
server {
gzip on;
gzip_disable "msie6";
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
}
```
- Так же желательно избавиться от дубля страницы www:
```bash
if ($host = 'www.example.com') {
return 301 https://example.com$request_uri;
}
```
- После настройки проверьте ваш конфигурационный файл на ошибки `nginx -t`, если ошибок нет, то перезапускаем сервис nginx `service nginx restart`.
- На сайте RUDVS добавьте свой домен и привяжите его к своему серверу, настройте DNS записи у регистратора. Через какое то время ваш магазин будет доступен по домену.
- Далее настройте SSL сертификат:
```bash
add-apt-repository ppa:certbot/certbot
apt-get update
apt-get install python-certbot-nginx
certbot --nginx -d yourdomain.com <--- в процессе установки выбираем цифру 2
```
- Ваш конфигурационный файл должен выглядеть примерно так:
```bash
server {
gzip on;
gzip_disable "msie6";
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name yourdomain.ru www.yourdomain.ru;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
if ($host = 'www.yourdomain.ru') {
return 301 https://yourdomain.ru$request_uri;
}
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourdomain.ru/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/yourdomain.ru/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.yourdomain.ru) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = yourdomain.ru) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80 default_server;
listen [::]:80 default_server;
server_name yourdomain.ru www.yourdomain.ru;
return 404; # managed by Certbot
}
```
### Установка скрипта на бесплатный хостинг от Heroku:
- Регистрируемся на сайте [Heroku](https://signup.heroku.com/login).
- Устанавливаем heroku-cli `https://devcenter.heroku.com/articles/heroku-cli`.
- Проверьте установку, написав в консоле `heroku --help` если всё выводит, идем дальше.
- Залогиньтесь: `heroku login`.
- Создайте новый проект Herkou: `heroku create`.
- Создайте кластер на сайте [www.mongodb.com](https://www.mongodb.com/) и импортируйте туда дамп структуры БД, который находится в корне приложения `/database_dump`. При подключении к БД выберите метод `Connect Your Application`
- Переходим на сайт heroku, идем в свое созданное приложение и открываем вкладку `Settings -> Config Vars` кнопка `Reveal Config Vars` и настраиваем [переменные среды](#Переменные-среды).
- Создаем папку с любым названием, открываем её и клонируем проект с github: `git clone https://github.com/sergeyyarkov/digital-store.git -b production .`
- Сменим ветку на master: `git checkout -b master`
- Удаляем файл ecosystem.config.js (в случае с heroku он не нужен т.к переменные мы храним у heroku)
- Добавляем удаленный репозиторий `heroku git:remote -a название вашего приложения`.
- Деплой:
```bash
git add .
git commit -m "make it better"
git push heroku master
```
### Настройка платежной системы:
В скрипте на данный момент доступна система оплаты через сервис QIWI, чтобы настроить оплату в вашем магазине следуйте следующим шагам:
- Перейдите на сайт [QIWI API](https://qiwi.com/p2p-admin/transfers/api) и создайте пару ключей авторизации. Сохраните секретный ключ.
- Если вы хотите настроить оформление формы приемов перевода, то перейдите [сюда](https://qiwi.com/p2p-admin/transfers/link) и скопируйте значение переменной `themeCode` и вставьте это значение по адресу `config/routes/payments/qiwi.payment.js`.
- Секретный ключ нужно указать в поле переменной среды `QIWI_SECRET_KEY`.
- Редирект после оплаты товара через QIWI на локальный адрес `localhost:3000` работать не будет, необходим домен.
### Настройка почтового сервиса:
Для отдачи данных товара на почту покупателя после покупки товара, нужно настроить nodemailer:
- Настройте почтовый сервис по адресу `config/nodemailer.config.js`:
```javascript
host: "smtp.mail.ru",
port: 465,
secure: true, // true для 465, false для остальных.
```
- Затем укажите переменные среды `EMAIL_LOGIN` и `EMAIL_PASSWORD`.
### Настройка системы комментариев:
В интернет магазине используется система комментариев [Disqus](https://disqus.com/):
- Добавьте свой домен на их сайт и замените embed код по адресу `dist/public/js/disqus.js`.
## Переменные среды:
Перменные среды нужны для работоспособности всего приложения, очень важно вписать верные данные для избежания возможных ошибок.
### Описание переменных:
| Переменная | Описание |
| --------------- | -------------------------------------------------------- |
| DATABASE_URI | Подключение к БД MongoDB |
| DATABASE_NAME | Имя БД |
| EMAIL_LOGIN | Логин от почты для отправки данных покупателю |
| EMAIL_PASSWORD | Пароль от почты |
| QIWI_SECRET_KEY | Секретный ключ QIWI для работоспособности системы оплаты |
`.env` файл:
```bash
DATABASE_URI=
DATABASE_NAME=
QIWI_SECRET_KEY=
EMAIL_LOGIN=
EMAIL_PASSWORD=
```
## API:
### API представляет из себя различные URL, которые отвечают данными только в формате JSON. Здесь описаны основные `GET` запросы на сервер для получения данных:
#### Для просмотра всех товаров которые существуют в магазине отправляется запрос:
```bash
https://store-name.com/api/items
```
Ответ:
```javascript
[
{
_id: "5e15dbc415303a1980cd77a0",
title: "Тестовый товар №1",
count: 0,
price: 1,
category: "steam",
description: "-",
date: "2020-01-08T13:40:20.353Z"
},
{
_id: "5e15dcb4138c3339bcbc3719",
title: "Тестовый товар №2",
count: 0,
price: 1,
category: "uplay",
description: "-",
date: "2020-01-08T13:44:20.940Z"
},
]
```
### Для просмотра товаров в интересующей категории отправляется запрос:
```bash
https://store-name.com/api/items/{category}
```
Ответ:
```javascript
[
{
_id: "5e15dbc415303a1980cd77a0",
title: "Тестовый товар №1",
count: 0,
price: 1,
category: "steam",
description: "-",
date: "2020-01-08T13:40:20.353Z"
},
{
_id: "5e1ccf30d5b894228c86385a",
title: "Тестовый товар №2",
count: 10,
price: 1,
category: "steam",
description: "-",
date: "2020-01-13T20:12:32.814Z"
}
]
```
### Для просмотра информации о всех категориях отправляется запрос:
```bash
https://store-name.com/api/categories
```
Ответ:
```javascript
[
{
_id: "5df64563309e691f2c33d400",
title: "uplay",
img: "img-1576349954743.svg",
type: "type",
format: "format"
},
{
_id: "5e04ea023625771abc75fd14",
title: "fortnite",
img: "img-1576349989720.svg",
type: "type",
format: "format"
},
]
```
### Для просмотра информации о интересующей категории отправляется запрос:
```bash
https://store-name.com/api/category/{category}
```
Ответ:
```javascript
[
{
_id: "5df64563309e691f2c33d400",
title: "uplay",
img: "img-1576349954743.svg",
type: "type",
format: "format"
}
]
```
### Для просмотра данных всех товаров отправляется запрос: **Нужна авторизация для администратора.**
```bash
https://store-name.com/api/items/data
```
Ответ:
```javascript
[
{
_id: "5e15dbc415303a1980cd77a0",
title: "Тестовый товар №1",
data: ["login:pass", "login:pass", "login:pass"]
},
{
_id: "5e175c63b479600ac493ce88",
title: "Тестовый товар №2",
data: ["login:pass", "login:pass", "login:pass"]
}
]
```
### Для просмотра покупателей отправляется запрос: **Нужна авторизация для администратора.**
```bash
https://store-name.com/api/buyers
```
Ответ:
```javascript
[
{
_id: "5e1c97701b619a31f010062c",
bill_id: "9244e5ae-560a-432a-9882-bc1b941c8cca",
email: "test-email@mail.com",
method: "qiwi",
date: "2020-01-13T18:13:38.623+03:00",
amount: 2,
data: ["login:pass", "login:pass"]
}
]
```
### Для просмотра списка иконок отправляется запрос: **Нужна авторизация для администратора.**
```bash
https://store-name.com/api/icons
```
Ответ:
```javascript
[
"img-1576349954743.svg",
"img-1576349989720.svg",
"img-1576420937629.svg",
"img-1576421960189.svg"
]
```
## Требования:
- Node.js v7.6.0 или выше
## Лицензия:
[MIT](https://github.com/sergeyyarkov/digital-store/blob/master/LICENSE)<br><br>

View File

@ -1,124 +0,0 @@
const ObjectID = require('mongodb').ObjectID;
module.exports = function (server, db) {
server.get('/', async (req, res) => {
try {
const store = await db.collection('content').findOne({});
db.collection('categories').find().toArray((err, result) => {
const categories = result.map(category => category.title[0].toUpperCase() + category.title.slice(1));
res.render('main/index', {
pageName: 'index',
title: `${store.title} | Главная`,
categories,
store: {
title: store.title,
info: store.info,
infoBottom: store.infoBottom
}
});
});
} catch {
res.status(500).render('main/404');
}
});
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
}
});
} 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
}
});
} 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
}
});
} 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
}
});
} 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: item.description,
date: item.date,
id: item._id,
type: category.type,
format: category.format,
store: {
title: store.title
}
});
}))
.catch(() => {
res.status(500).render('main/404');
});
});
}

View File

@ -0,0 +1,7 @@
{
"_id" : ObjectId("5de9733dc58a41381415a7b0"),
"id" : "1575580477024",
"name" : "Administrator",
"email" : "admin@support.ru",
"password" : "$2a$10$l5A1VHuN3a9IXhPq.tK.WOWvrIDZJxu1LKIPOYe6ywlS4dbjIMBu6"
}

11
database_dump/buyers.json Normal file
View File

@ -0,0 +1,11 @@
{
"_id" : ObjectId("5e2c6e4770964b0017bb9009"),
"bill_id" : "51076c5e-25d7-422e-8f73-9bc3f583679c",
"email" : "testmail@mail.com",
"method" : "qiwi",
"date" : "2020-01-25T19:35:14.025+03:00",
"amount" : NumberInt(1),
"data" : [
"login:pass"
]
}

View File

@ -0,0 +1,28 @@
{
"_id" : ObjectId("5df63ed5044ecc18c43e9e91"),
"title" : "steam",
"img" : "img-1576420937629.svg",
"type" : "Steam Type",
"format" : "Steam Format"
},
{
"_id" : ObjectId("5df64563309e691f2c33d400"),
"title" : "uplay",
"img" : "img-1576349954743.svg",
"type" : "Uplay Type",
"format" : "Uplay Format"
},
{
"_id" : ObjectId("5e04ea023625771abc75fd14"),
"title" : "fortnite",
"img" : "img-1576349989720.svg",
"type" : "Fortnite Type",
"format" : "Fortnite Format"
},
{
"_id" : ObjectId("5e11c381471ae208f0b6aae4"),
"title" : "origin",
"img" : "img-1576421960189.svg",
"type" : "Origin Type",
"format" : "Origin Format"
}

View File

@ -0,0 +1,11 @@
{
"_id" : ObjectId("5e0b7466fbd02d01c46038a7"),
"title" : "Store Name",
"email" : "email@support.ru",
"info" : "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?",
"infoBottom" : "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?",
"howToBuy" : "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?",
"contacts" : "1. Почта: email@support.ru\r\n2. Telegram: @support.ru",
"recommendations" : "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?",
"color" : "#26a69a"
}

64
database_dump/info.json Normal file
View File

@ -0,0 +1,64 @@
{
"_id" : ObjectId("5e15dbc415303a1980cd77a0"),
"title" : "Тестовый товар №1",
"data" : [
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass"
]
},
{
"_id" : ObjectId("5e15dcb4138c3339bcbc3719"),
"title" : "Тестовый товар №3",
"data" : [
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass"
]
},
{
"_id" : ObjectId("5e175c63b479600ac493ce88"),
"title" : "Тестовый товар №4",
"data" : [
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass"
]
},
{
"_id" : ObjectId("5e1ccf30d5b894228c86385a"),
"title" : "Тестовый товар №5",
"data" : [
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass",
"login:pass"
]
}

36
database_dump/items.json Normal file
View File

@ -0,0 +1,36 @@
{
"_id" : ObjectId("5e15dbc415303a1980cd77a0"),
"title" : "Тестовый товар №1",
"count" : NumberInt(10),
"price" : NumberInt(1),
"category" : "steam",
"description" : "Тестовый товар №1 описание",
"date" : ISODate("2020-01-08T13:40:20.353+0000")
},
{
"_id" : ObjectId("5e15dcb4138c3339bcbc3719"),
"title" : "Тестовый товар №3",
"count" : NumberInt(10),
"price" : NumberInt(1),
"category" : "uplay",
"description" : "Тестовый товар №3 fdsfd",
"date" : ISODate("2020-01-08T13:44:20.940+0000")
},
{
"_id" : ObjectId("5e175c63b479600ac493ce88"),
"title" : "Тестовый товар №4",
"count" : NumberInt(10),
"price" : NumberInt(1),
"category" : "fortnite",
"description" : "Тестовый товар №4 opis",
"date" : ISODate("2020-01-09T17:01:23.446+0000")
},
{
"_id" : ObjectId("5e1ccf30d5b894228c86385a"),
"title" : "Тестовый товар №5",
"count" : NumberInt(10),
"price" : NumberInt(1),
"category" : "origin",
"description" : "sdsdsds",
"date" : ISODate("2020-01-13T20:12:32.814+0000")
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

8
dist/public/js/disqus.js vendored Normal file
View File

@ -0,0 +1,8 @@
(function() {
var d = document, s = d.createElement('script');
s.src = 'https://justatest-2.disqus.com/embed.js';
if (document.querySelector('#disqus_thread')) {
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
}
})();

File diff suppressed because one or more lines are too long

View File

@ -1,3 +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:8080/admin.js") script(src="http://localhost:8084/admin.js")

View File

@ -11,7 +11,7 @@ header.admin-header
.user-view .user-view
.background .background
img(src="/img/admin/background-user.jpg", alt="#") img(src="/img/admin/background-user.jpg", alt="#")
span(style="padding-top: 5px;padding-bottom:10px;display:block;").white-text Control Panel v1.0.0 span(style="padding-top: 5px;padding-bottom:10px;display:block;").white-text Панель управления
a(href="#") a(href="#")
img(class="circle" src="/img/admin/avatar-user.jpg", alt="alt") img(class="circle" src="/img/admin/avatar-user.jpg", alt="alt")
a(href="#") a(href="#")
@ -40,21 +40,25 @@ header.admin-header
.collapsible-body .collapsible-body
ul ul
li(data-page="items") li(data-page="items")
a(href='http://'+host+'/control-panel/items') a(href='/control-panel/items')
i.material-icons list i.material-icons list
| Товары | Товары
li(data-page="categories") li(data-page="categories")
a(href='http://'+host+'/control-panel/categories') a(href='/control-panel/categories')
i.material-icons category i.material-icons category
| Категории | Категории
li(data-page="icons") li(data-page="icons")
a(href='http://'+host+'/control-panel/icons') a(href='/control-panel/icons')
i.material-icons add_photo_alternate i.material-icons add_photo_alternate
| Иконки | Иконки
li(data-page="content") li(data-page="content")
a(href='http://'+host+'/control-panel/content') a(href='/control-panel/content')
i.material-icons visibility i.material-icons visibility
| Контент на сайте | Контент на сайте
li(data-page="theme")
a(href='/control-panel/theme')
i.material-icons create
| Оформление
ul.collapsible ul.collapsible
li li
a(href="#" class="collapsible-header waves-effect") a(href="#" class="collapsible-header waves-effect")
@ -63,7 +67,7 @@ header.admin-header
.collapsible-body .collapsible-body
ul ul
li(data-page="administrators") li(data-page="administrators")
a(href='http://'+host+'/control-panel/administrators') a(href='/control-panel/administrators')
i.material-icons supervisor_account i.material-icons supervisor_account
| Администратор | Администратор
//- Модальные окна //- Модальные окна

View File

@ -0,0 +1,31 @@
// Edit Item
#billDataModal.c_modal-wrap.hide
.container
.row
.c_modal
.c_modal-content
.modal-content
.modal-content__header
h4.small-title Информация о заказе
span#billDataClose.modal-close
i.material-icons close
.modal-content__form
.input-field
input#bill_id(type="text" readonly)
label Номер заказа
.input-field
input#email(type="text" readonly)
label Почта
.input-field
input#method(type="text" readonly)
label Способ оплаты
.input-field
input#date(type="text" readonly)
label Дата оплаты
.input-field
input#amount(type="text" readonly)
label Сумма
.input-field
textarea#data(class="materialize-textarea" type="text" name="type" readonly)
label Данные аккаунтов
// Edit Category END

View File

@ -2,6 +2,16 @@ include ./assets/head.pug
body.has-fixed-sidenav body.has-fixed-sidenav
include ./assets/templates/header.pug include ./assets/templates/header.pug
main.admin-main main.admin-main
#loader.loader-fullscreen.hide
.loader-fullscreen__wrap
.preloader-wrapper.big.active
.spinner-layer.spinner-green-only
.circle-clipper.left
.circle
.gap-patch
.circle
.circle-clipper.right
.circle
.container .container
.row .row
.col.s12 .col.s12

View File

@ -23,6 +23,9 @@ body.has-fixed-sidenav
.input-field.col.s12 .input-field.col.s12
textarea(id="contacts" class="materialize-textarea" type="text" name="contacts") #{store.contacts} textarea(id="contacts" class="materialize-textarea" type="text" name="contacts") #{store.contacts}
label(for="contacts") Контакты магазина label(for="contacts") Контакты магазина
.input-field.col.s12
textarea(id="recommendations" class="materialize-textarea" type="text" name="recommendations") #{store.recommendations}
label(for="recommendations") Рекомендации к покупке аккаунтов
.input-field.col.s12 .input-field.col.s12
textarea(id="info" class="materialize-textarea" type="text" name="info") #{store.info} textarea(id="info" class="materialize-textarea" type="text" name="info") #{store.info}
label(for="info") Блок с важной информацией label(for="info") Блок с важной информацией

View File

@ -2,6 +2,16 @@ include ./assets/head.pug
body.has-fixed-sidenav body.has-fixed-sidenav
include ./assets/templates/header.pug include ./assets/templates/header.pug
main.admin-main main.admin-main
#loader.loader-fullscreen.hide
.loader-fullscreen__wrap
.preloader-wrapper.big.active
.spinner-layer.spinner-green-only
.circle-clipper.left
.circle
.gap-patch
.circle
.circle-clipper.right
.circle
.container .container
.row .row
.col.s12 .col.s12
@ -43,10 +53,10 @@ body.has-fixed-sidenav
th Способ оплаты th Способ оплаты
th Дата оплаты th Дата оплаты
th Сумма th Сумма
th Информация о покупке
.divider .divider
tbody tbody
#loader.progress ul#pagination.pagination.center(data-page="1")
.indeterminate include ./assets/templates/modals/bill_data.pug
ul#pagination.pagination.center(data-page="1")
include ./assets/templates/footer.pug include ./assets/templates/footer.pug
include ./assets/scripts.pug include ./assets/scripts.pug

View File

@ -17,7 +17,7 @@ body.has-fixed-sidenav
.file-field.input-field.col.s12 .file-field.input-field.col.s12
.btn.orange .btn.orange
span Иконка span Иконка
input(type="file" name="img" required) input(type="file" accept="image/*" name="img" required)
.file-path-wrapper .file-path-wrapper
input(type="text" class="file-path" placeholder="Загрузите иконку в формате .svg") input(type="text" class="file-path" placeholder="Загрузите иконку в формате .svg")
button(type="submit" class="btn right" style="margin-right: 10px;") Добавить button(type="submit" class="btn right" style="margin-right: 10px;") Добавить

View File

@ -2,6 +2,16 @@ include ./assets/head.pug
body.has-fixed-sidenav body.has-fixed-sidenav
include ./assets/templates/header.pug include ./assets/templates/header.pug
main.admin-main main.admin-main
#loader.loader-fullscreen.hide
.loader-fullscreen__wrap
.preloader-wrapper.big.active
.spinner-layer.spinner-green-only
.circle-clipper.left
.circle
.gap-patch
.circle
.circle-clipper.right
.circle
.container .container
.row .row
.col.s12 .col.s12
@ -32,8 +42,6 @@ body.has-fixed-sidenav
.divider .divider
tbody tbody
#loader.progress
.indeterminate
ul#pagination.pagination.center(data-page="1") ul#pagination.pagination.center(data-page="1")
.divider .divider
.items-add .items-add

24
dist/views/admin/theme.pug vendored Normal file
View File

@ -0,0 +1,24 @@
include ./assets/head.pug
body.has-fixed-sidenav
include ./assets/templates/header.pug
main.admin-main
.container
.row
.col.s12
h2 Изменить оформление
p Измените оформление вашего магазине на этой странице.
.col.s12
#theme.card
.card-content
.theme-change
h5 Общие цвета магазина
.row
form(action="/control-panel/theme/update" method="POST")
#color.input-field.col.s12
input(id="color" type="text" pattern="^#(?:[0-9a-fA-F]{3}){1,2}$" name="color" value=color required)
label(for="color") Цвет в формате HEX (#000000)
.edit_success.col.s12
button(type="submit" class="btn waves-effect") Сохранить
include ./assets/templates/footer.pug
include ./assets/scripts.pug

View File

@ -13,4 +13,16 @@ html(lang='ru')
link(rel="stylesheet", href="/css/media.css") link(rel="stylesheet", href="/css/media.css")
// Material Icons // Material Icons
link(rel="stylesheet", href="https://fonts.googleapis.com/icon?family=Material+Icons") link(rel="stylesheet", href="https://fonts.googleapis.com/icon?family=Material+Icons")
if store
style.
nav, footer.main-footer, .items-category, .category-items li .active, .category-items li a:hover, .progress .indeterminate, button {
background-color: #{store.color} !important;
}
button:hover {
background-color: #{store.color} !important;
opacity: .7;
}
.progress {
background-color: #deecea;
}
body body

View File

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

View File

@ -18,10 +18,7 @@ main.main-section
hr hr
p p
strong Отзывы strong Отзывы
p &nbsp; #disqus_thread
p (тут блок с отзывами)
//- Footer Template //- Footer Template
include ./assets/templates/footer.pug include ./assets/templates/footer.pug

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

@ -6,6 +6,17 @@ include ./assets/templates/header.pug
main.main-section main.main-section
include ./assets/templates/modals/cart.pug include ./assets/templates/modals/cart.pug
include ./assets/templates/modals/pay.pug include ./assets/templates/modals/pay.pug
#notification
#loader.loader-fullscreen.hide
.loader-fullscreen__wrap
.preloader-wrapper.big.active
.spinner-layer.spinner-green-only
.circle-clipper.left
.circle
.gap-patch
.circle
.circle-clipper.right
.circle
.container .container
.row .row
.col.s12 .col.s12
@ -17,13 +28,13 @@ main.main-section
.orders-text .orders-text
hr hr
p p
strong Введите почту strong Введите вашу почту и мы вышлем вам ваши товары которые вы оплачивали
p &nbsp; p &nbsp;
form(action='#') form(action='#')
.email-field(style='max-width: 400px;margin-bottom: 10px;') .email-field(style='margin-bottom: 10px;')
label(for='email') Почта label(for='email') Почта
input#email(required='' type='text' name='email') input#email(required='' type='email' name='email')
.show-orders .show-orders(style='display: flex;justify-content: flex-end;')
button.waves-effect.waves-light.btn(type='submit') Узнать button.waves-effect.waves-light.btn(type='submit') Узнать

View File

@ -39,22 +39,18 @@ 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
| Lorem ipsum dolor sit amet consectetur adipisicing elit. A saepe ad, atque
| cum officia ullam similique repudiandae excepturi at accusamus hic deserunt h3 Отзывы о товаре
| molestiae mollitia? Labore numquam officiis natus repellat fugit ipsum #disqus_thread
| nesciunt, nam officia? Sunt ut quod provident dolore architecto laborum ab
| repellat ea sequi optio dolorum, tenetur quis eius rerum quibusdam nisi
| minus, debitis non. Impedit molestiae beatae, fugit similique sit labore quo
| incidunt id corporis quia officiis pariatur.
//- Footer Template //- Footer Template
include ./assets/templates/footer.pug include ./assets/templates/footer.pug

4843
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,9 @@
"name": "digital-store", "name": "digital-store",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "server/server.js",
"scripts": { "scripts": {
"start": "node config/server.js", "start": "node server/server.js",
"build": "webpack --mode production", "build": "webpack --mode production",
"wp-server": "webpack-dev-server --mode development" "wp-server": "webpack-dev-server --mode development"
}, },
@ -23,12 +23,13 @@
"@qiwi/bill-payments-node-js-sdk": "^3.1.3", "@qiwi/bill-payments-node-js-sdk": "^3.1.3",
"axios": "^0.19.0", "axios": "^0.19.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^3.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-random-string": "^3.1.0",
"express": "^4.17.1", "express": "^4.17.1",
"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,7 +1,5 @@
const bcrypt = require('bcryptjs');
const passport = require('passport'); const passport = require('passport');
const initializePassport = require('../config/passport.config');
const initializePassport = require('../passport.config');
function checkNotAuthenticated(req, res, next) { function checkNotAuthenticated(req, res, next) {
if (req.isAuthenticated()) return res.redirect('/control-panel'); if (req.isAuthenticated()) return res.redirect('/control-panel');

View File

@ -57,6 +57,15 @@ module.exports = function(server, db) {
} }
}); });
server.get('/api/items/id/:id', async (req, res) => {
try {
const item = await db.collection('items').findOne({_id: ObjectID(req.params.id)})
res.json(item)
} catch {
res.status(500).render('main/404');
}
});
server.get('/api/items/page/:page', async (req, res) => { server.get('/api/items/page/:page', async (req, res) => {
try { try {
const page = parseInt(req.params.page), const page = parseInt(req.params.page),
@ -77,10 +86,23 @@ module.exports = function(server, db) {
} }
}); });
server.get('/api/items/data/:id', checkAuthenticated, async (req, res) => {
try {
const data = await db.collection('info').findOne({_id: ObjectID(req.params.id)});
res.json(data)
} catch {
res.status(500).render('main/404');
}
})
server.get('/api/buyers', checkAuthenticated, async (req, res) => { server.get('/api/buyers', checkAuthenticated, async (req, res) => {
try { try {
const buyers = await db.collection('buyers').find({}).toArray(); const buyers = await db.collection('buyers').find({}).toArray();
res.json(buyers); if (buyers.length > 0) {
res.json(buyers);
} else res.send({
error: 'Покупателей в магазине пока что нет.'
});
} catch { } catch {
res.status(500).render('main/404'); res.status(500).render('main/404');
} }
@ -88,7 +110,7 @@ module.exports = function(server, db) {
server.get('/api/buyers/:bill_id', checkAuthenticated, async (req, res) => { server.get('/api/buyers/:bill_id', checkAuthenticated, async (req, res) => {
try { try {
const buyers = await db.collection('buyers').find({bill_id: req.params.bill_id}).toArray(); const buyers = await db.collection('buyers').findOne({bill_id: req.params.bill_id});
res.json(buyers); res.json(buyers);
} catch { } catch {
res.status(500).render('main/404'); res.status(500).render('main/404');
@ -157,6 +179,15 @@ module.exports = function(server, db) {
} }
}); });
server.get('/api/category/id/:id', async (req, res) => {
try {
const category = await db.collection('categories').findOne({_id: ObjectID(req.params.id)});
res.json(category)
} catch {
res.status(500).render('main/404');
}
})
server.get('/api/icons', checkAuthenticated, (req, res) => { server.get('/api/icons', checkAuthenticated, (req, res) => {
try { try {
const icons = fs.readdirSync('dist/public/img/service-icons'); const icons = fs.readdirSync('dist/public/img/service-icons');

View File

@ -119,7 +119,8 @@ module.exports = function (server, db) {
info: store.info, info: store.info,
infoBottom: store.infoBottom, infoBottom: store.infoBottom,
howToBuy: store.howToBuy, howToBuy: store.howToBuy,
contacts: store.contacts contacts: store.contacts,
recommendations: store.recommendations
} }
}); });
} catch { } catch {
@ -142,6 +143,23 @@ module.exports = function (server, db) {
} }
}); });
server.get('/control-panel/theme', checkAuthenticated, async (req, res) => {
try {
const store = await db.collection('content').findOne({});
res.render('admin/theme', {
id: req.user.id,
name: req.user.name,
email: req.user.email,
title: `${store.title} | Оформление`,
host: req.headers.host,
pageName: ['Оформление', 'theme'],
color: store.color
});
} catch {
res.status(500).render('main/404');
}
});
// редактирование категорий // редактирование категорий
server.post('/control-panel/categories/create', checkAuthenticated, (req, res) => { server.post('/control-panel/categories/create', checkAuthenticated, (req, res) => {
try { try {
@ -159,8 +177,7 @@ module.exports = function (server, db) {
}); });
server.post('/control-panel/categories/delete', checkAuthenticated, (req, res) => { server.post('/control-panel/categories/delete', checkAuthenticated, (req, res) => {
try { try {
const data = JSON.parse(req.body.category); db.collection('categories').deleteOne({"_id": ObjectID(req.body.category)});
db.collection('categories').deleteOne({"_id": ObjectID(data.id)});
res.redirect('/control-panel/categories'); res.redirect('/control-panel/categories');
} catch { } catch {
res.status(500).render('main/404'); res.status(500).render('main/404');
@ -203,15 +220,16 @@ module.exports = function (server, db) {
}); });
// товары // товары
server.post('/control-panel/items/create', checkAuthenticated, (req, res) => { server.post('/control-panel/items/create', checkAuthenticated, async (req, res) => {
try { try {
const category = await db.collection('categories').findOne({_id: ObjectID(req.body.category)})
// документ с информацией товара для отдачи клиенту // документ с информацией товара для отдачи клиенту
const item = { const item = {
_id: new ObjectID(), _id: new ObjectID(),
title: req.body.title, title: req.body.title,
count: req.body.data.split(',').length, count: req.body.data.split(',').length,
price: parseFloat(req.body.price), price: parseFloat(req.body.price),
category: JSON.parse(req.body.category).title, category: category.title,
description: req.body.description, description: req.body.description,
date: new Date() date: new Date()
} }
@ -239,17 +257,18 @@ module.exports = function (server, db) {
res.render('main/404'); res.render('main/404');
} }
}); });
server.post('/control-panel/items/update', checkAuthenticated, (req, res) => { server.post('/control-panel/items/update', checkAuthenticated, async (req, res) => {
try { try {
const category = await db.collection('categories').findOne({_id: ObjectID(req.body.category)})
const data = { const data = {
id: req.body.id, id: req.body.id,
title: req.body.title, title: req.body.title,
price: parseFloat(req.body.price), price: parseFloat(req.body.price),
description: req.body.description, description: req.body.description,
category: JSON.parse(req.body.category), category,
info: req.body.data.split(',') info: req.body.data.split(',')
} }
db.collection('items').updateOne({"_id": ObjectID(data.id)}, {$set: {title: data.title, price: data.price, description: data.description, category: data.category.title}}); db.collection('items').updateOne({"_id": ObjectID(data.id)}, {$set: {title: data.title, price: data.price, description: data.description, category: category.title}});
db.collection('info').updateOne({"_id": ObjectID(data.id)}, {$set: {title: data.title, data: data.info}}); db.collection('info').updateOne({"_id": ObjectID(data.id)}, {$set: {title: data.title, data: data.info}});
res.redirect('/control-panel/items'); res.redirect('/control-panel/items');
} catch { } catch {
@ -266,9 +285,10 @@ module.exports = function (server, db) {
email: req.body.email, email: req.body.email,
infoBottom: req.body.infoBottom, infoBottom: req.body.infoBottom,
howToBuy: req.body.howToBuy, howToBuy: req.body.howToBuy,
contacts: req.body.contacts contacts: req.body.contacts,
recommendations: req.body.recommendations
} }
db.collection('content').updateOne({}, {$set: {title: data.title, email: data.email, info: data.info, infoBottom: data.infoBottom, howToBuy: data.howToBuy, contacts: data.contacts}}); db.collection('content').updateOne({}, {$set: {title: data.title, email: data.email, info: data.info, infoBottom: data.infoBottom, howToBuy: data.howToBuy, contacts: data.contacts, recommendations: data.recommendations}});
res.redirect('/control-panel/content'); res.redirect('/control-panel/content');
} catch { } catch {
res.status(500).render('main/404'); res.status(500).render('main/404');
@ -297,5 +317,18 @@ module.exports = function (server, db) {
} catch { } catch {
res.status(500).render('main/404'); res.status(500).render('main/404');
} }
}) });
// формление
server.post('/control-panel/theme/update', checkAuthenticated, (req, res) => {
try {
const data = {
color: req.body.color
}
db.collection('content').updateOne({}, {$set: {color: data.color}});
res.redirect('/control-panel/theme');
} catch {
res.status(500).render('main/404');
}
});
} }

178
server/routes/main.js Normal file
View File

@ -0,0 +1,178 @@
const ObjectID = require('mongodb').ObjectID
const marked = require('marked')
const nodemailer = require('../config/nodemailer.config')
module.exports = function (server, db) {
server.get('/', async (req, res) => {
try {
const store = await db.collection('content').findOne({})
db.collection('categories')
.find()
.toArray((err, result) => {
const categories = result.map(
(category) =>
category.title[0].toUpperCase() + category.title.slice(1)
)
res.render('main/index', {
pageName: 'index',
title: `${store.title} | Главная`,
categories,
store: {
title: store.title,
info: marked(store.info),
infoBottom: marked(store.infoBottom),
color: store.color,
},
})
})
} catch {
res.status(500).render('main/404')
}
})
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: marked(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: marked(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) => {
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>Дата оплаты: ${new Date(
buyer.date
).toLocaleDateString()}</b><br>
<b>Данные товара:</b><br>
<p>${buyer.data.join('<br>')}</p>
<p>======================================================================</p>
`
})
// высылаем данные покупателю
try {
await nodemailer.transporter.sendMail({
from: `"Ваши покупки." <${process.env.EMAIL_LOGIN}>`,
to: req.body.email,
subject: 'Данные купленных товаров находятся в этом письме.',
html: html.join(''),
})
res.json({ status: 'OK' })
} catch (error) {
res.json({ status: 'ERR' })
}
} else {
res.json({ status: 'NF' })
}
})
}

View File

@ -23,7 +23,7 @@ module.exports = function(server, db, qiwiApi) {
themeCode: 'Sergei-YaS7mIY0CQe' themeCode: 'Sergei-YaS7mIY0CQe'
}, },
email: req.body.email, email: req.body.email,
successUrl: `https://polar-peak-95205.herokuapp.com/success?bill_id=${billId}&item_id=${ids.join('and')}&payment=qiwi` successUrl: `${req.headers.origin}/success?bill_id=${billId}&item_id=${ids.join('and')}&payment=qiwi`
} }
qiwiApi.createBill(billId, fields).then((data) => { qiwiApi.createBill(billId, fields).then((data) => {
res.send(data); res.send(data);

View File

@ -1,5 +1,5 @@
const ObjectID = require('mongodb').ObjectID; const ObjectID = require('mongodb').ObjectID;
const nodemailer = require('../nodemailer.config'); const nodemailer = require('../config/nodemailer.config');
module.exports = function(server, db, qiwiApi) { module.exports = function(server, db, qiwiApi) {
server.get('/success', async (req, res) => { server.get('/success', async (req, res) => {
@ -34,14 +34,16 @@ module.exports = function(server, db, qiwiApi) {
db.collection('buyers').insertOne({bill_id, email, method, date, amount, data: result}); db.collection('buyers').insertOne({bill_id, email, method, date, amount, data: result});
await nodemailer.transporter.sendMail({ await nodemailer.transporter.sendMail({
from: `"Оплата аккаунта." <${process.env.EMAIL_LOGIN}>`, // sender address from: `"Оплата аккаунта." <${process.env.EMAIL_LOGIN}>`,
to: email, // list of receivers to: email,
subject: "Оплата успешно проведена.", // subject subject: "Оплата успешно проведена.",
text: "Данные купленного товара находятся в этом письме.", // plain text body text: "Данные купленного товара находятся в этом письме.",
html: `<b>Номер заказа: ${bill_id}</b><br> html: `
<b>Номер заказа: ${bill_id}</b><br>
<b>Дата оплаты: ${new Date(data.status.changedDateTime).toLocaleDateString()}</b><br>
<b>Данные товара:</b><br> <b>Данные товара:</b><br>
<p>${result.join('<br>')}</p> <p>${result.join('<br>')}</p>
` // html body `
}); });
res.render('main/success', {result, email}); res.render('main/success', {result, email});

View File

@ -11,7 +11,6 @@ const path = require('path');
const passport = require('passport'); const passport = require('passport');
const flash = require('express-flash'); const flash = require('express-flash');
const session = require('express-session'); const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const methodOverride = require('method-override'); const methodOverride = require('method-override');
const multer = require('multer'); const multer = require('multer');
const QiwiBillPaymentsAPI = require('@qiwi/bill-payments-node-js-sdk'); const QiwiBillPaymentsAPI = require('@qiwi/bill-payments-node-js-sdk');
@ -24,23 +23,23 @@ MongoClient.connect(db_config.url, {useNewUrlParser: true, useUnifiedTopology: t
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: 'dist/public/img/service-icons', destination: 'dist/public/img/service-icons',
filename: function(req, file, cb) { filename: function(req, file, cb) {
cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname)); cb(null, `${file.fieldname}-${Date.now()}${path.extname(file.originalname)}`);
} }
}); });
// Config // Config
server.use(cors()) server.use(cors());
server.use(express.static(path.join(__dirname, '../dist/public'))); server.use(express.static(`${__dirname}/../dist/public`));
server.use(multer({storage: storage}).single('img')); server.use(multer({storage: storage}).single('img'));
server.use(express.urlencoded({ extended: false })); server.use(express.urlencoded({ extended: false }));
server.use(express.json()); server.use(express.json());
server.use(flash()); server.use(flash());
server.use(session({secret: process.env.SESSION_SECRET,resave: false,saveUninitialized: false})); server.use(session({secret: require('crypto-random-string')({length: 64, type: 'base64'}), resave: true,saveUninitialized: false}));
server.use(passport.initialize()); server.use(passport.initialize());
server.use(passport.session()); server.use(passport.session());
server.use(methodOverride('_method')); server.use(methodOverride('_method'));
server.set('view engine', 'pug'); server.set('view engine', 'pug');
server.set('views', path.join(__dirname, '../dist/views')); server.set('views', `${__dirname}/../dist/views`);
// Routes // Routes
require('./routes/main')(server, db); // маршруты сайта require('./routes/main')(server, db); // маршруты сайта
@ -57,13 +56,8 @@ MongoClient.connect(db_config.url, {useNewUrlParser: true, useUnifiedTopology: t
}) })
.catch((error) => { .catch((error) => {
server.get('*', (req, res) => { server.get('*', (req, res) => {
res.send('Ошибка соединения с БД.'); res.send('Произошла ошибка при запуске. Используйте команду "pm2 log" для того чтобы узнать ошибку.');
console.log(error); console.log(error);
}); });
}) })
.finally(() => { .finally(server.listen(process.env.PORT || 3000, console.log('App is running...')));
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log('App is running...');
});
});

View File

@ -5,88 +5,131 @@ export class PaginationComponent extends Component {
super(id); super(id);
this.loader = loader; this.loader = loader;
} }
/**
*
* @param {number} length - кол-во товаров в магазине
* @param {object} body - куда вставляем данные о первой странице
* @param {function} request - функция async запроса
*/
// вставляем кнопки со страницами и назначаем ивент initialize(length, body, request) {
initialize(pages, insertTo, request) { // рендерим кнопку с первой страницой как независимую от других кнопок
this.insertButtons(pages); 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.$pages = Array.from(this.$el.querySelectorAll('li'));
this.$el.onclick = (e) => this.buttonHandler(e, insertTo, request); // рендерим сами кнопки и кнопки вперед - назад
this.moveTo(1, insertTo, request); // показываем первую страницу this.renderBtns(length);
this.$el.querySelector('li').nextElementSibling.classList.add('active'); this.renderArrows();
// показ первой страницы
this.move(1, body, request);
// назначаем ивент на кнопки
this.$el.onclick = (e) => this.buttonHandler(e, body, request);
} }
removeActive() { // страницу назад - вперед
this.$pages.forEach(li => li.classList.remove('active')); 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);
} }
setActive(li, value) { renderBtns(length) {
li.forEach(li => {const page = parseInt(li.querySelector('a').dataset.move);page === value ? li.classList.add('active') : false;}); 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);
} }
buttonHandler(e, insertTo, request) { 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(); e.preventDefault();
if (e.target.dataset.move) { if (e.target.dataset.move) {
const page = parseInt(e.target.dataset.move), const page = parseInt(e.target.dataset.move),
target = e.target.parentNode; target = e.target.parentNode;
this.removeActive(); this.move(page, body, request);
target.classList.add('active');
this.moveTo(page, insertTo, request);
} else if (e.target.tagName.toLowerCase() === 'i') { } else if (e.target.tagName.toLowerCase() === 'i') {
const moveType = e.target.parentNode.dataset.type, const type = e.target.parentNode.dataset.type,
page = parseInt(this.$el.dataset.page); page = parseInt(this.$el.dataset.page);
moveType === 'prev' ? this.movePrev(page, this.$pages, insertTo, request) : this.moveNext(page, this.$pages, insertTo, request); switch (type) {
case 'next':
this.next(page, body, request);
break;
case 'prev':
this.prev(page, body, request);
default:
break;
}
} }
} }
movePrev(page, li, insertTo, request) { next(page, body, request) {
if (page === 1) return 0; else { this.move(++page, body, request);
--page;
this.removeActive();
this.setActive(li, page);
this.moveTo(page, insertTo, request);
}
} }
// выполняем запрос на сервер и выводим на данные prev(page, body, request) {
async moveTo(page, insertTo, request) { --page;
this.loader.show(); if (page < 1) {
insertTo.innerHTML = ''; return 0;
const response = await request(page);
if (response != '') {
this.$el.setAttribute('data-page', page);
insertTo.insertAdjacentHTML('afterbegin', response);
} else { } else {
insertTo.innerHTML = 'Данных нету.'; this.move(page, body, request);
} }
this.loader.hide();
}
moveNext(page, li, insertTo, request) {
const length = this.$pages.length - 2;
if (page === length) return 0; else {
++page;
this.removeActive();
this.setActive(li, page);
this.moveTo(page, insertTo, request);
}
}
insertButtons(pages) {
const count = Math.ceil(pages / 5);
let html = '';
// назад
html += '<li><a class="disabled" href="#" data-type="prev"><i class="material-icons">chevron_left</i></a></li>';
// все страницы
for (let i = 1; i <= count; i++) {
html += `<li><a href="#" data-move="${i}">${i}</a></li>`;
}
// вперед
html += '<li><a class="disabled" href="#" data-type="next"><i class="material-icons">chevron_right</i></a></li>';
this.$el.insertAdjacentHTML('afterbegin', html);
} }
} }

View File

@ -11,6 +11,7 @@ import { CategoryEditModal } from "./modals/category-edit.modal";
import { ContentComponent } from "./components/content.component.js"; import { ContentComponent } from "./components/content.component.js";
import { AdministratorComponent } from "./components/administrator.component.js"; import { AdministratorComponent } from "./components/administrator.component.js";
import { DashboardComponent } from "./components/dashboard.component.js"; import { DashboardComponent } from "./components/dashboard.component.js";
import { BillDataModal } from "./modals/bill-data.modal.js";
// Components // Components
const loader = new LoaderComponent('loader'); const loader = new LoaderComponent('loader');
@ -24,9 +25,10 @@ const content = new ContentComponent('content');
const administrator = new AdministratorComponent('administrator'); const administrator = new AdministratorComponent('administrator');
// Modals // Modals
const itemEdit = new ItemEditModal('editItemModal', 'edit', 'editItemClose'); const itemEdit = new ItemEditModal('editItemModal', 'edit', 'editItemClose', {loader});
const itemDelete = new ItemDeleteModal('deleteItemModal', 'edit', 'deleteItemClose'); const itemDelete = new ItemDeleteModal('deleteItemModal', 'edit', 'deleteItemClose');
const categoryEdit = new CategoryEditModal('editCategoryModal', 'editCategory_select', 'editCategoryClose'); const categoryEdit = new CategoryEditModal('editCategoryModal', 'editCategory_select', 'editCategoryClose', {loader});
const billData = new BillDataModal('billDataModal', 'dashboard', 'billDataClose', {loader});
const materialize = new MaterializeConfig(); const materialize = new MaterializeConfig();
console.log('Control-Panel v1.0.0'); 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

@ -3,8 +3,9 @@ import { apiService } from "../services/api.service";
import { renderIcons } from "../templates/icons.template"; import { renderIcons } from "../templates/icons.template";
export class CategoryEditModal extends Modal { export class CategoryEditModal extends Modal {
constructor(id, open, close) { constructor(id, open, close, {loader}) {
super(id, open, close); super(id, open, close);
this.loader = loader;
} }
// перепишем метод для перевода ивента open в событие onchange // перепишем метод для перевода ивента open в событие onchange
@ -27,19 +28,25 @@ export class CategoryEditModal extends Modal {
} }
async openHandler(e) { async openHandler(e) {
const data = JSON.parse(e.target.value), try {
icons = await apiService.getIcons(); this.loader.show();
const icons = await apiService.getIcons(),
this.$img.innerHTML = `<option value='${data.img}' selected>Оставить прежнюю</option>`; category = await apiService.getCategoryById(e.target.value)
this.$id.value = data.id;
this.$originalTitle.value = data.title[0].toUpperCase() + data.title.slice(1); this.$img.innerHTML = `<option value='${category.img}' selected>Оставить прежнюю</option>`;
this.$title.value = data.title[0].toUpperCase() + data.title.slice(1); this.$id.value = category._id;
this.$type.value = data.type; this.$originalTitle.value = category.title[0].toUpperCase() + category.title.slice(1);
this.$format.value = data.format; this.$title.value = category.title[0].toUpperCase() + category.title.slice(1);
this.$img.insertAdjacentHTML('beforeend', renderIcons(icons)); this.$type.value = category.type;
M.FormSelect.init(this.$img); this.$format.value = category.format;
M.textareaAutoResize(this.$type); this.$img.insertAdjacentHTML('beforeend', renderIcons(icons));
M.updateTextFields(); M.FormSelect.init(this.$img);
M.textareaAutoResize(this.$type);
M.updateTextFields();
this.loader.hide();
} catch (error) {
alert('Произошла ошибка, обновите страницу')
}
} }
async updateCategory(e) { async updateCategory(e) {

View File

@ -1,4 +1,5 @@
import { Modal } from "../core/modal"; import { Modal } from "../core/modal";
import { apiService } from "../services/api.service";
import axios from "axios"; import axios from "axios";
export class ItemDeleteModal extends Modal { export class ItemDeleteModal extends Modal {
@ -19,10 +20,16 @@ export class ItemDeleteModal extends Modal {
this.$button = this.$el.querySelector('.delete-item'); this.$button = this.$el.querySelector('.delete-item');
} }
openHandler(e) { async openHandler(e) {
const data = JSON.parse(e.target.parentNode.parentNode.dataset.info); try {
this.$content.innerHTML = `Вы действительно хотите удалить товар "${data.title}"? Находящиеся в нем аккаунты будут удалены.`; const id = e.target.parentNode.parentNode.dataset.id,
this.$button.onclick = this.deleteHandler.bind(this, data.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) { async deleteHandler(id) {

View File

@ -3,8 +3,9 @@ import { apiService } from "../services/api.service";
import { renderCategories } from "../templates/categories.template"; import { renderCategories } from "../templates/categories.template";
export class ItemEditModal extends Modal { export class ItemEditModal extends Modal {
constructor(id, open, close) { constructor(id, open, close, {loader}) {
super(id, open, close); super(id, open, close);
this.loader = loader
} }
show(e) { show(e) {
@ -25,19 +26,29 @@ export class ItemEditModal extends Modal {
} }
async openHandler(e) { async openHandler(e) {
const data = JSON.parse(e.target.parentNode.parentNode.dataset.info), try {
categories = await apiService.getCategories(); 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='{"title": "${data.category}"}' selected>Оставить прежнюю</option>`; this.$category.innerHTML = `<option value='${category[0]._id}' selected>Оставить прежнюю</option>`;
this.$id.value = data.id; this.$id.value = item._id;
this.$title.value = data.title; this.$title.value = item.title;
this.$price.value = data.price; this.$price.value = item.price;
this.$description.value = data.description; this.$description.value = item.description;
this.$category.insertAdjacentHTML('beforeend', renderCategories(categories)); this.$category.insertAdjacentHTML('beforeend', renderCategories(categories));
this.$data.value = data.data; this.$data.value = item.data;
M.FormSelect.init(this.$category); M.FormSelect.init(this.$category);
M.textareaAutoResize(this.$description); M.textareaAutoResize(this.$description);
M.textareaAutoResize(this.$data); M.textareaAutoResize(this.$data);
M.updateTextFields(); M.updateTextFields();
this.loader.hide();
} catch (error) {
alert('Произошла ошибка, обновите страницу')
}
} }
} }

View File

@ -13,6 +13,15 @@ class ApiService {
console.error(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() { async getCategories() {
try { try {
@ -56,6 +65,24 @@ class ApiService {
} }
} }
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) { async getItemsOne(category, sorting) {
try { try {
if (sorting) { if (sorting) {
@ -88,6 +115,15 @@ class ApiService {
} }
} }
async getBuyerById(id) {
try {
const request = `${this.url}/api/buyers/${id}`;
return useRequest(request);
} catch (error) {
console.log(error)
}
}
async getBuyersOffset(page) { async getBuyersOffset(page) {
try { try {
const request = `${this.url}/api/buyers/page/${page}`; const request = `${this.url}/api/buyers/page/${page}`;
@ -121,4 +157,4 @@ async function useRequest(request) {
return await JSON.parse(response.request.response); return await JSON.parse(response.request.response);
} }
export const apiService = new ApiService('http://localhost:3000'); export const apiService = new ApiService(document.defaultView.location.origin);

View File

@ -1,7 +1,7 @@
export function renderBuyers(buyers) { export function renderBuyers(buyers) {
let html = ''; if (!buyers.error) {
buyers.forEach(buyer => { 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('');
html += `<tr><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></tr>`; } else {
}); return 'Покупателей в магазине пока что нету';
return html; }
} }

View File

@ -1,7 +1,3 @@
export function renderCategories(categories) { export function renderCategories(categories) {
let html = ''; 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('');
categories.forEach(category => {
html += `<option value='{"id": "${category._id}", "title": "${category.title}", "img": "${category.img}", "type": "${category.type}", "format": "${category.format}"}' data-icon="../img/service-icons/${category.img}">${category.title[0].toUpperCase() + category.title.slice(1)}</option>`;
});
return html;
} }

View File

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

View File

@ -1,13 +1,13 @@
export function renderItems(items, categories, info) { export function renderItems(items, categories, info) {
let html = '';
if (!items.error) { if (!items.error) {
categories.forEach(category => { return categories.map(category => {
items.forEach((item, i) => { return items.map((item, i) => {
if (item.category.toLowerCase() === category.title.toLowerCase()) html += `<tr data-info='{"id": "${item._id}", "title": "${item.title}", "category": "${item.category}", "price": "${item.price}", "description": "${item.description}", "data": "${info[i].data}"}'><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><button id="deleteItem" class="btn red">Удалить</button><button id="editItem" class="btn orange">Изменить</button></td></tr>` 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>`;
}); }
} else html += 'Данных нету.'; }).join('');
}).join('');
return html; } else {
return 'Данных нету.';
}
} }

View File

@ -1,120 +1,119 @@
import { Component } from "../core/component"; import { Component } from '../core/component'
import { apiService } from "../services/api.service"; import { apiService } from '../services/api.service'
import { insertCount } from "../other/counter"; import { insertCount } from '../other/counter'
import { renderItems } from "../templates/items.template"; import { renderItems } from '../templates/items.template'
export class ItemsComponent extends Component { export class ItemsComponent extends Component {
constructor(id, {loader}, {notification}) { constructor(id, { loader }, { notification }) {
super(id); super(id)
this.loader = loader; this.loader = loader
this.notification = notification; this.notification = notification
this.$el ? this.roster = this.$el.querySelector('#roster') : false; this.$el ? (this.roster = this.$el.querySelector('#roster')) : false
if (localStorage.getItem('cart')) { if (localStorage.getItem('cart')) {
this.cart = JSON.parse(localStorage.getItem('cart')); this.cart = JSON.parse(localStorage.getItem('cart'))
} else { } else {
this.cart = []; 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'
} }
})
} }
}
async init() { insertItems(html) {
if (this.$el) { this.roster.insertAdjacentHTML('afterbegin', html)
localStorage.setItem('sortByAll', 'true'); // установка значения для компонента Filter this.btnColor()
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) { function addItem(e) {
if (e.target.localName === 'button' && e.target.id === 'addItem') {
if (e.target.localName === 'button' && e.target.id === 'addItem') { const target = e.target
const target = e.target; const item = {
const item = { id: e.target.parentNode.dataset.id,
id: e.target.parentNode.dataset.id, title: e.target.parentNode.dataset.title,
title: e.target.parentNode.dataset.title, count: e.target.parentNode.dataset.count,
count: e.target.parentNode.dataset.count, price: e.target.parentNode.dataset.price,
price: e.target.parentNode.dataset.price
}
renderObj(item, this, target);
} else if (e.target.localName === 'i') {
const target = e.target.parentNode;
const item = {
id: e.target.parentNode.parentNode.dataset.id,
title: e.target.parentNode.parentNode.dataset.title,
count: e.target.parentNode.parentNode.dataset.count,
price: e.target.parentNode.parentNode.dataset.price
}
renderObj(item, this, target);
} }
renderObj(item, this, target)
}
} }
function renderObj(obj, context, target) { function renderObj(obj, context, target) {
if (context.cart.length > 0) { if (context.cart.length > 0) {
let insterted = false; let insterted = false
let index; let index
// Проверка на существующий айтем в корзине // Проверка на существующий айтем в корзине
context.cart.forEach((item, i) => { context.cart.forEach((item, i) => {
if (item.id === obj.id) { if (item.id === obj.id) {
insterted = true; insterted = true
index = i; index = i
} }
}); })
if (insterted === false) { if (insterted === false) {
context.cart.push(obj) context.cart.push(obj)
localStorage.setItem('cart', JSON.stringify(context.cart)); localStorage.setItem('cart', JSON.stringify(context.cart))
target.classList.add('orange'); target.classList.add('orange')
target.querySelector('i').innerHTML = 'close'; target.querySelector('i').innerHTML = 'close'
insertCount('counter', context.cart); insertCount('counter', context.cart)
context.notification.notificate('Товар был успешно добавлен в корзину.', '#26A69A'); 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 { } else {
context.cart.push(obj); context.cart.splice(index, 1)
localStorage.setItem('cart', JSON.stringify(context.cart)); localStorage.setItem('cart', JSON.stringify(context.cart))
target.classList.add('orange'); target.classList.remove('orange')
target.querySelector('i').innerHTML = 'close'; target.querySelector('i').innerHTML = 'shopping_cart'
insertCount('counter', context.cart); insertCount('counter', context.cart)
context.notification.notificate('Товар был успешно добавлен в корзину.', '#26A69A');
} }
}; } 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

@ -5,16 +5,16 @@ export class NotificationComponent extends Component {
super(id); super(id);
} }
notificate(text, color) { notificate(text, color, delay) {
const card = document.createElement('div'); const card = document.createElement('div');
card.classList.add('card-panel'); card.classList.add('card-panel');
card.style.backgroundColor = color; card.style.backgroundColor = color;
card.innerHTML = `<span class="white-text">${text}</span>`; card.innerHTML = `<span class="white-text">${text}</span>`;
this.$el.innerHTML += card.outerHTML; this.$el.innerHTML += card.outerHTML;
this.removeNotificate(); this.removeNotificate(delay);
} }
removeNotificate() { removeNotificate(delay) {
// Полифилл для IE ^9 // Полифилл для IE ^9
(function() { (function() {
var arr = [window.Element, window.CharacterData, window.DocumentType]; var arr = [window.Element, window.CharacterData, window.DocumentType];
@ -45,6 +45,6 @@ export class NotificationComponent extends Component {
setTimeout(() => { setTimeout(() => {
this.$el.querySelector('.card-panel').remove(); this.$el.querySelector('.card-panel').remove();
}, 2000) }, 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

@ -9,6 +9,7 @@ import { CartModal } from "./modals/cart.modal.js";
import { PayModal } from "./modals/pay.modal.js"; import { PayModal } from "./modals/pay.modal.js";
import { NotificationComponent } from "./components/notification.component.js"; import { NotificationComponent } from "./components/notification.component.js";
import { MaterializeConfig } from "./config/materialize.config.js"; import { MaterializeConfig } from "./config/materialize.config.js";
import { OrdersComponent } from "./components/orders.component.js";
// Components // Components
const header = new HeaderComponent('header'); const header = new HeaderComponent('header');
@ -19,6 +20,7 @@ const items = new ItemsComponent('items', {loader}, {notification});
const filter = new FilterComponent('filter', {items}); const filter = new FilterComponent('filter', {items});
const category = new CategoryComponent('category', {items}, {filter}); const category = new CategoryComponent('category', {items}, {filter});
const breadcumb = new BreadcrumbComponent('breadcrumb'); const breadcumb = new BreadcrumbComponent('breadcrumb');
const orders = new OrdersComponent('orders', {notification}, {loader});
// Modals // Modals
const cart = new CartModal('cart', 'cartOpen', 'cartClose', {items}); const cart = new CartModal('cart', 'cartOpen', 'cartClose', {items});

View File

@ -58,4 +58,4 @@ async function useRequest(request) {
return await JSON.parse(response.request.response); return await JSON.parse(response.request.response);
} }
export const apiService = new ApiService('http://localhost:3000'); export const apiService = new ApiService(document.defaultView.location.origin);