mirror of
https://github.com/sergeyyarkov/digital-store.git
synced 2026-10-11 10:19:15 +03:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 020d8599b9 | |||
| 84338c9566 | |||
| 7282c740f4 | |||
| 3a77ac81ab | |||
| ec2cdc0e5b | |||
| 2728293110 | |||
| 3a17fa5235 | |||
| 0c964e2135 | |||
| af04a2619e | |||
| 73929244f4 | |||
| 3f68af1521 | |||
| d4e1eca030 | |||
| 104073cffe | |||
| 7f321d0ddb | |||
| e43ea0f34b | |||
| 2623087082 | |||
| b44bb97b8b | |||
| e4fa09f07c | |||
| c20037d8ed | |||
| c2b6e7c31c | |||
| aaa085c416 | |||
| 2b4f4610a5 | |||
| 8e79ce1e99 | |||
| c444ac2cfd | |||
| 3140c3a8ee | |||
| a63ec56170 | |||
| 308dfb34de | |||
| 316bab9ad5 | |||
| edd81cb451 | |||
| 7d7e2ebccb | |||
| ad4ff4f0f1 | |||
| 70ebf728fe | |||
| 6d6df14b27 | |||
| 332db3e55f | |||
| 3a0e325481 | |||
| a7d581dc96 | |||
|
|
21cade8a17 | ||
| 8ec78a9fae | |||
| c469d8d56f | |||
| b7b67214ab | |||
| 01b66f81f0 | |||
| 629df78a22 |
9
.babelrc
9
.babelrc
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
["@babel/preset-env", {
|
|
||||||
"targets": [
|
|
||||||
"last 5 versions"
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
|
||||||
.env
|
.env
|
||||||
492
README.md
492
README.md
@ -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>
|
||||||
|
|||||||
7
database_dump/administrators.json
Normal file
7
database_dump/administrators.json
Normal 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
11
database_dump/buyers.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
28
database_dump/categories.json
Normal file
28
database_dump/categories.json
Normal 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"
|
||||||
|
}
|
||||||
11
database_dump/content.json
Normal file
11
database_dump/content.json
Normal 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
64
database_dump/info.json
Normal 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
36
database_dump/items.json
Normal 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")
|
||||||
|
}
|
||||||
84
dist/public/css/style.css
vendored
84
dist/public/css/style.css
vendored
@ -45,10 +45,78 @@ hr {
|
|||||||
|
|
||||||
/* Custom Styles */
|
/* Custom Styles */
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity:0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity:1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-moz-keyframes fadein { /* Firefox */
|
||||||
|
from {
|
||||||
|
opacity:0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity:1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes fadein { /* Safari and Chrome */
|
||||||
|
from {
|
||||||
|
opacity:0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity:1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-o-keyframes fadein { /* Opera */
|
||||||
|
from {
|
||||||
|
opacity:0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#commento {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-fullscreen {
|
||||||
|
background-color: #000;
|
||||||
|
background-color: rgba(0,0,0,.8);
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
z-index: 99999999;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
animation: fadein 0.1s;
|
||||||
|
-moz-animation: fadein 0.1s; /* Firefox */
|
||||||
|
-webkit-animation: fadein 0.1s; /* Safari and Chrome */
|
||||||
|
-o-animation: fadein 0.1s; /* Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-fullscreen__wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#notification > div {
|
||||||
|
animation: fadein 0.2s;
|
||||||
|
-moz-animation: fadein 0.2s; /* Firefox */
|
||||||
|
-webkit-animation: fadein 0.2s; /* Safari and Chrome */
|
||||||
|
-o-animation: fadein 0.2s; /* Opera */
|
||||||
|
}
|
||||||
|
|
||||||
#notification {
|
#notification {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -94,6 +162,14 @@ hr {
|
|||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#rosterButtons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button#addItem {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Cart */
|
/* Cart */
|
||||||
.cart-btn {
|
.cart-btn {
|
||||||
@ -435,6 +511,12 @@ footer.main-footer .container {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.items-list__name {
|
||||||
|
max-width: 525px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.items-list__name,
|
.items-list__name,
|
||||||
.items-list__info {
|
.items-list__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -710,10 +792,10 @@ footer.main-footer {
|
|||||||
}
|
}
|
||||||
.table-buyers, .table-items {
|
.table-buyers, .table-items {
|
||||||
max-height: 385px;
|
max-height: 385px;
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
}
|
||||||
#items .card-content {
|
#items .card-content {
|
||||||
min-height: 519px;
|
min-height: 519px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
footer.admin-footer{
|
footer.admin-footer{
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|||||||
4
dist/public/js/admin.js
vendored
4
dist/public/js/admin.js
vendored
File diff suppressed because one or more lines are too long
8
dist/public/js/disqus.js
vendored
Normal file
8
dist/public/js/disqus.js
vendored
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
4
dist/public/js/main.js
vendored
4
dist/public/js/main.js
vendored
File diff suppressed because one or more lines are too long
3
dist/views/admin/assets/scripts.pug
vendored
3
dist/views/admin/assets/scripts.pug
vendored
@ -1,3 +1,2 @@
|
|||||||
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")
|
|
||||||
16
dist/views/admin/assets/templates/header.pug
vendored
16
dist/views/admin/assets/templates/header.pug
vendored
@ -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
|
||||||
| Администратор
|
| Администратор
|
||||||
//- Модальные окна
|
//- Модальные окна
|
||||||
|
|||||||
31
dist/views/admin/assets/templates/modals/bill_data.pug
vendored
Normal file
31
dist/views/admin/assets/templates/modals/bill_data.pug
vendored
Normal 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
|
||||||
10
dist/views/admin/categories.pug
vendored
10
dist/views/admin/categories.pug
vendored
@ -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
|
||||||
|
|||||||
3
dist/views/admin/content.pug
vendored
3
dist/views/admin/content.pug
vendored
@ -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") Блок с важной информацией
|
||||||
|
|||||||
18
dist/views/admin/control-panel.pug
vendored
18
dist/views/admin/control-panel.pug
vendored
@ -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
|
||||||
2
dist/views/admin/icons.pug
vendored
2
dist/views/admin/icons.pug
vendored
@ -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;") Добавить
|
||||||
|
|||||||
12
dist/views/admin/items.pug
vendored
12
dist/views/admin/items.pug
vendored
@ -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
24
dist/views/admin/theme.pug
vendored
Normal 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
|
||||||
12
dist/views/main/assets/head.pug
vendored
12
dist/views/main/assets/head.pug
vendored
@ -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
|
||||||
4
dist/views/main/assets/scripts.pug
vendored
4
dist/views/main/assets/scripts.pug
vendored
@ -1,3 +1,3 @@
|
|||||||
// Script JS
|
// Script JS
|
||||||
//script(src='../js/main.js')
|
script(src='../js/main.js')
|
||||||
script(src='http://localhost:8080/main.js')
|
script(src="../js/disqus.js")
|
||||||
5
dist/views/main/comments.pug
vendored
5
dist/views/main/comments.pug
vendored
@ -18,10 +18,7 @@ main.main-section
|
|||||||
hr
|
hr
|
||||||
p
|
p
|
||||||
strong Отзывы
|
strong Отзывы
|
||||||
p
|
#disqus_thread
|
||||||
p (тут блок с отзывами)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//- Footer Template
|
//- Footer Template
|
||||||
include ./assets/templates/footer.pug
|
include ./assets/templates/footer.pug
|
||||||
19
dist/views/main/my-orders.pug
vendored
19
dist/views/main/my-orders.pug
vendored
@ -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
|
p
|
||||||
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') Узнать
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
dist/views/main/product.pug
vendored
12
dist/views/main/product.pug
vendored
@ -47,14 +47,10 @@ main.main-section
|
|||||||
p #{format}
|
p #{format}
|
||||||
hr
|
hr
|
||||||
h3 Рекомендации к покупке аккаунтов.
|
h3 Рекомендации к покупке аккаунтов.
|
||||||
p
|
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
|
||||||
21
ecosystem.config.js
Normal file
21
ecosystem.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps : [{
|
||||||
|
name: 'server',
|
||||||
|
script: 'server/server.js',
|
||||||
|
autorestart: true,
|
||||||
|
watch: true,
|
||||||
|
instances: 'max',
|
||||||
|
exec_mode: 'cluster',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'development'
|
||||||
|
},
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
DATABASE_URI: 'mongodb://127.0.0.1:27017',
|
||||||
|
DATABASE_NAME: '',
|
||||||
|
EMAIL_LOGIN: '',
|
||||||
|
EMAIL_PASSWORD: '',
|
||||||
|
QIWI_SECRET_KEY: ''
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
};
|
||||||
12
index.html
12
index.html
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Webpack output javascript</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Webpack output javascript</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
31
package-lock.json
generated
31
package-lock.json
generated
@ -2246,14 +2246,6 @@
|
|||||||
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
|
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"connect-mongo": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-0Mx88079Z20CG909wCFlR3UxhMYGg6Ibn1hkIje1hwsqOLWtL9HJV+XD0DAjUvQScK6WqY/FA8tSVQM9rR64Rw==",
|
|
||||||
"requires": {
|
|
||||||
"mongodb": "^3.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"console-browserify": {
|
"console-browserify": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
|
||||||
@ -2453,10 +2445,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"crypto-random-string": {
|
"crypto-random-string": {
|
||||||
"version": "1.0.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.1.0.tgz",
|
||||||
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=",
|
"integrity": "sha512-Tip3yGB+bA7B0W8E4K4mNf2rZhu5r2G5Tb89/utEl5tP1QuLjTF/S9a1b8ifDrR4ORc9Utf6tscpSEtBY3YcPQ==",
|
||||||
"dev": true
|
"requires": {
|
||||||
|
"type-fest": "^0.8.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cyclist": {
|
"cyclist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -7666,6 +7660,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||||
},
|
},
|
||||||
|
"type-fest": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
|
||||||
|
},
|
||||||
"type-is": {
|
"type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
@ -7823,6 +7822,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"crypto-random-string": "^1.0.0"
|
"crypto-random-string": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-random-string": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
|
|||||||
10
package.json
10
package.json
@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "digital-store",
|
"name": "digital-store",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"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",
|
|
||||||
"wp-server": "webpack-dev-server --mode development"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -23,8 +21,8 @@
|
|||||||
"@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",
|
||||||
|
|||||||
@ -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');
|
||||||
@ -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');
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
const ObjectID = require('mongodb').ObjectID;
|
const ObjectID = require('mongodb').ObjectID;
|
||||||
|
const nodemailer = require('../config/nodemailer.config');
|
||||||
|
|
||||||
module.exports = function (server, db) {
|
module.exports = function (server, db) {
|
||||||
server.get('/', async (req, res) => {
|
server.get('/', async (req, res) => {
|
||||||
@ -13,7 +14,8 @@ module.exports = function (server, db) {
|
|||||||
store: {
|
store: {
|
||||||
title: store.title,
|
title: store.title,
|
||||||
info: store.info,
|
info: store.info,
|
||||||
infoBottom: store.infoBottom
|
infoBottom: store.infoBottom,
|
||||||
|
color: store.color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -29,7 +31,8 @@ module.exports = function (server, db) {
|
|||||||
title: `${store.title} | Как купить товар`,
|
title: `${store.title} | Как купить товар`,
|
||||||
store: {
|
store: {
|
||||||
title: store.title,
|
title: store.title,
|
||||||
howToBuy: store.howToBuy
|
howToBuy: store.howToBuy,
|
||||||
|
color: store.color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -44,7 +47,8 @@ module.exports = function (server, db) {
|
|||||||
title: `${store.title} | Контакты`,
|
title: `${store.title} | Контакты`,
|
||||||
store: {
|
store: {
|
||||||
title: store.title,
|
title: store.title,
|
||||||
contacts: store.contacts
|
contacts: store.contacts,
|
||||||
|
color: store.color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -58,7 +62,8 @@ module.exports = function (server, db) {
|
|||||||
pageName: 'comments',
|
pageName: 'comments',
|
||||||
title: `${store.title} | Отзывы`,
|
title: `${store.title} | Отзывы`,
|
||||||
store: {
|
store: {
|
||||||
title: store.title
|
title: store.title,
|
||||||
|
color: store.color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -72,7 +77,8 @@ module.exports = function (server, db) {
|
|||||||
pageName: 'my-orders',
|
pageName: 'my-orders',
|
||||||
title: `${store.title} | Мои покупки`,
|
title: `${store.title} | Мои покупки`,
|
||||||
store: {
|
store: {
|
||||||
title: store.title
|
title: store.title,
|
||||||
|
color: store.color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -113,7 +119,9 @@ module.exports = function (server, db) {
|
|||||||
type: category.type,
|
type: category.type,
|
||||||
format: category.format,
|
format: category.format,
|
||||||
store: {
|
store: {
|
||||||
title: store.title
|
title: store.title,
|
||||||
|
recommendations: store.recommendations,
|
||||||
|
color: store.color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}))
|
}))
|
||||||
@ -121,4 +129,34 @@ module.exports = function (server, db) {
|
|||||||
res.status(500).render('main/404');
|
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'});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
@ -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});
|
||||||
@ -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...');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { Component } from '../core/component';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
|
|
||||||
export class AdministratorComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$el.querySelector('form').addEventListener('submit', this.request.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
const data = {
|
|
||||||
id: e.target.elements.id.value,
|
|
||||||
name: e.target.elements.name.value,
|
|
||||||
email: e.target.elements.email.value,
|
|
||||||
oldPass: e.target.elements.oldPass.value,
|
|
||||||
newPass: e.target.elements.newPass.value
|
|
||||||
}
|
|
||||||
const response = await axios.post('/control-panel/administrators/update', data);
|
|
||||||
alert(response.data);
|
|
||||||
document.location.reload(true);
|
|
||||||
} catch (error) {
|
|
||||||
alert('Невозможно выполнить запрос.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderCategories } from "../templates/categories.template";
|
|
||||||
import { renderIcons } from "../templates/icons.template";
|
|
||||||
|
|
||||||
export class CategoriesComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
// селекты в которых рендерится контент
|
|
||||||
this.$editSelect = this.$el.querySelector('#editCategory_select'); // редактирование категории
|
|
||||||
this.$dellSelect = this.$el.querySelector('#deleteCategory_select'); // удаление категории
|
|
||||||
this.$iconsSelect = this.$el.querySelector('#icons_select'); // выбор иконки
|
|
||||||
|
|
||||||
// обработка форм
|
|
||||||
this.$addForm = this.$el.querySelector('#addCategory');
|
|
||||||
this.$dellForm = this.$el.querySelector('#deleteCategory');
|
|
||||||
|
|
||||||
// добавим ивенты на наши формы
|
|
||||||
this.$addForm.addEventListener('submit', this.addCategory.bind(this));
|
|
||||||
this.$dellForm.addEventListener('submit', this.deleteCategory.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const categories = await apiService.getCategories(),
|
|
||||||
icons = await apiService.getIcons();
|
|
||||||
|
|
||||||
this.$editSelect.insertAdjacentHTML('beforeend', renderCategories(categories));
|
|
||||||
this.$dellSelect.insertAdjacentHTML('beforeend', renderCategories(categories));
|
|
||||||
this.$iconsSelect.insertAdjacentHTML('beforeend', renderIcons(icons));
|
|
||||||
M.FormSelect.init(document.querySelectorAll('select'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async addCategory(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const categories = await apiService.getCategories(),
|
|
||||||
title = this.$addForm.elements[0].value.toLowerCase().trim(),
|
|
||||||
index = categories.map((category) => {return category.title}).indexOf(title);
|
|
||||||
|
|
||||||
index != -1 ? alert('Такая категория уже существует, придумайте другое название.') : this.$addForm.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteCategory(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const categories = await apiService.getCategories(),
|
|
||||||
title = this.$dellForm.elements[0].value.toLowerCase().trim(),
|
|
||||||
index = categories.map((category) => {return category.title}).indexOf(title);
|
|
||||||
|
|
||||||
index != -1 ? this.$dellForm.submit() : alert('Выберите категорию для удаления.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { Component } from '../core/component';
|
|
||||||
|
|
||||||
export class ContentComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$info = this.$el.querySelector('#info');
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
M.textareaAutoResize(this.$info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderBuyers } from "../templates/buyers.template";
|
|
||||||
|
|
||||||
export class DashboardComponent extends Component {
|
|
||||||
constructor(id, {pagination}, {search}) {
|
|
||||||
super(id);
|
|
||||||
this.pagination = pagination;
|
|
||||||
this.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const buyers = await apiService.getBuyers();
|
|
||||||
this.$table = this.$el.querySelector('table tbody');
|
|
||||||
this.search.initialize(this.$table, this.getBuyersByBillId);
|
|
||||||
this.pagination.initialize(buyers.length, this.$table, this.getBuyersByPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBuyersByBillId(bill_id) {
|
|
||||||
const buyer = await apiService.getBillId(bill_id);
|
|
||||||
return renderBuyers(buyer);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBuyersByPage(page) {
|
|
||||||
const buyers = await apiService.getBuyersOffset(page);
|
|
||||||
return renderBuyers(buyers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderIcons } from "../templates/icons.template";
|
|
||||||
|
|
||||||
export class IconsComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$icons = this.$el.querySelector('#selectIcons');
|
|
||||||
this.$delete = this.$el.querySelector('#dellIcon');
|
|
||||||
this.$delete.addEventListener('submit', this.dellIcon.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const icons = await apiService.getIcons();
|
|
||||||
this.$icons.insertAdjacentHTML('beforeend', renderIcons(icons));
|
|
||||||
M.FormSelect.init(this.$icons);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dellIcon(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const icons = await apiService.getIcons(),
|
|
||||||
currIcon = e.target.elements[0].value,
|
|
||||||
index = icons.indexOf(currIcon);
|
|
||||||
|
|
||||||
index != -1 ? e.target.submit() : alert('Выберите иконку для удаления.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderItems } from "../templates/items.template";
|
|
||||||
import { renderCategories } from "../templates/categories.template";
|
|
||||||
|
|
||||||
export class ItemsComponent extends Component {
|
|
||||||
constructor(id, {pagination}, {search}) {
|
|
||||||
super(id);
|
|
||||||
this.pagination = pagination;
|
|
||||||
this.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$categories = this.$el.querySelector('#selectCategories');
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const items = await apiService.getItems(),
|
|
||||||
categories = await apiService.getCategories();
|
|
||||||
|
|
||||||
this.$table = this.$el.querySelector('table tbody');
|
|
||||||
this.search.initialize(this.$table, this.getItemsByText);
|
|
||||||
this.pagination.initialize(items.length, this.$table, this.getItemsByPage);
|
|
||||||
this.$categories.insertAdjacentHTML('beforeend', renderCategories(categories));
|
|
||||||
M.FormSelect.init(this.$categories);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsByText(text) {
|
|
||||||
const items = await apiService.getItemsQuery(text),
|
|
||||||
categories = await apiService.getCategories(),
|
|
||||||
data = await apiService.getItemsData();
|
|
||||||
return renderItems(items, categories, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsByPage(page) {
|
|
||||||
const items = await apiService.getItemsOffset(page),
|
|
||||||
categories = await apiService.getCategories(),
|
|
||||||
data = await apiService.getItemsData();
|
|
||||||
return renderItems(items, categories, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class LoaderComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class PaginationComponent extends Component {
|
|
||||||
constructor(id, {loader}) {
|
|
||||||
super(id);
|
|
||||||
this.loader = loader;
|
|
||||||
}
|
|
||||||
|
|
||||||
// вставляем кнопки со страницами и назначаем ивент
|
|
||||||
initialize(pages, insertTo, request) {
|
|
||||||
this.insertButtons(pages);
|
|
||||||
this.$pages = Array.from(this.$el.querySelectorAll('li'));
|
|
||||||
this.$el.onclick = (e) => this.buttonHandler(e, insertTo, request);
|
|
||||||
this.moveTo(1, insertTo, request); // показываем первую страницу
|
|
||||||
this.$el.querySelector('li').nextElementSibling.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
removeActive() {
|
|
||||||
this.$pages.forEach(li => li.classList.remove('active'));
|
|
||||||
}
|
|
||||||
|
|
||||||
setActive(li, value) {
|
|
||||||
li.forEach(li => {const page = parseInt(li.querySelector('a').dataset.move);page === value ? li.classList.add('active') : false;});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonHandler(e, insertTo, request) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.target.dataset.move) {
|
|
||||||
const page = parseInt(e.target.dataset.move),
|
|
||||||
target = e.target.parentNode;
|
|
||||||
|
|
||||||
this.removeActive();
|
|
||||||
target.classList.add('active');
|
|
||||||
this.moveTo(page, insertTo, request);
|
|
||||||
} else if (e.target.tagName.toLowerCase() === 'i') {
|
|
||||||
const moveType = e.target.parentNode.dataset.type,
|
|
||||||
page = parseInt(this.$el.dataset.page);
|
|
||||||
|
|
||||||
moveType === 'prev' ? this.movePrev(page, this.$pages, insertTo, request) : this.moveNext(page, this.$pages, insertTo, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
movePrev(page, li, insertTo, request) {
|
|
||||||
if (page === 1) return 0; else {
|
|
||||||
--page;
|
|
||||||
this.removeActive();
|
|
||||||
this.setActive(li, page);
|
|
||||||
this.moveTo(page, insertTo, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// выполняем запрос на сервер и выводим на данные
|
|
||||||
async moveTo(page, insertTo, request) {
|
|
||||||
this.loader.show();
|
|
||||||
insertTo.innerHTML = '';
|
|
||||||
const response = await request(page);
|
|
||||||
if (response != '') {
|
|
||||||
this.$el.setAttribute('data-page', page);
|
|
||||||
insertTo.insertAdjacentHTML('afterbegin', response);
|
|
||||||
} else {
|
|
||||||
insertTo.innerHTML = 'Данных нету.';
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class SearchComponent extends Component {
|
|
||||||
constructor(id, {loader}) {
|
|
||||||
super(id);
|
|
||||||
this.loader = loader;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(insertTo, request) {
|
|
||||||
this.$el.onchange = (e) => {
|
|
||||||
this.search(e, insertTo, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async search(e, insertTo, request) {
|
|
||||||
try {
|
|
||||||
insertTo.innerHTML = '';
|
|
||||||
this.loader.show();
|
|
||||||
const query = e.target.value.trim(),
|
|
||||||
items = await request(query);
|
|
||||||
|
|
||||||
items != '' ? insertTo.insertAdjacentHTML('afterbegin', items) : insertTo.innerHTML = `По запросу "${query}" ничего не найдено...`;
|
|
||||||
e.target.value = '';
|
|
||||||
this.loader.hide();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Произошла ошибка.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
const M = require('../frameworks/materialize.min');
|
|
||||||
export class MaterializeConfig {
|
|
||||||
constructor() {
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Closest полифилл для ie11
|
|
||||||
(function() {
|
|
||||||
if (!Element.prototype.closest) {
|
|
||||||
Element.prototype.closest = function(css) {
|
|
||||||
var node = this;
|
|
||||||
|
|
||||||
while (node) {
|
|
||||||
if (node.matches(css)) return node;
|
|
||||||
else node = node.parentElement;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// Matches полифилл для ie11
|
|
||||||
(function() {
|
|
||||||
if (!Element.prototype.matches) {
|
|
||||||
Element.prototype.matches = Element.prototype.matchesSelector ||
|
|
||||||
Element.prototype.webkitMatchesSelector ||
|
|
||||||
Element.prototype.mozMatchesSelector ||
|
|
||||||
Element.prototype.msMatchesSelector;
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
M.Sidenav.init(document.querySelectorAll('.sidenav'));
|
|
||||||
M.Collapsible.init(document.querySelectorAll('.collapsible'));
|
|
||||||
M.Modal.init(document.querySelectorAll('.modal'));
|
|
||||||
document.querySelector('#type') ? M.textareaAutoResize(document.querySelector('#type')) : false
|
|
||||||
M.updateTextFields();
|
|
||||||
|
|
||||||
// окраска li по нажатию
|
|
||||||
if (document.querySelector('#currentPage')) {
|
|
||||||
const currentPage = document.querySelector('#currentPage').dataset.current;
|
|
||||||
const pages = Array.from(document.querySelectorAll('.collapsible li'));
|
|
||||||
pages.forEach(function (page) {
|
|
||||||
if (page.dataset.page) {
|
|
||||||
if (currentPage === page.dataset.page) {
|
|
||||||
document.querySelector('#home').classList.remove('active');
|
|
||||||
page.classList.add('active');
|
|
||||||
const collapse = M.Collapsible.getInstance(page.closest('.collapsible'));
|
|
||||||
collapse.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
export class Component {
|
|
||||||
constructor(id) {
|
|
||||||
this.$el = document.getElementById(id);
|
|
||||||
this.$el ? this.setElems() : false;
|
|
||||||
this.$el ? this.init() : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {}
|
|
||||||
|
|
||||||
setElems() {}
|
|
||||||
|
|
||||||
onShow() {}
|
|
||||||
|
|
||||||
onHide() {}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.$el ? this.$el.classList.add('hide') : false;
|
|
||||||
this.onHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
this.$el ? this.$el.classList.remove('hide') : false;
|
|
||||||
this.onShow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
export class Modal {
|
|
||||||
constructor(id, open, close) {
|
|
||||||
this.$el = document.getElementById(id);
|
|
||||||
this.open = document.getElementById(open);
|
|
||||||
this.close = document.getElementById(close);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.open) {
|
|
||||||
this.setElems();
|
|
||||||
this.open.addEventListener('click', this.show.bind(this));
|
|
||||||
this.close.addEventListener('click', this.hide.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
show(e) {
|
|
||||||
this.$el.classList.remove('hide');
|
|
||||||
document.body.style.overflowY = 'hidden';
|
|
||||||
this.openHandler(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.$el.classList.add('hide');
|
|
||||||
document.body.style.overflowY = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
openHandler() {}
|
|
||||||
|
|
||||||
setElems() {}
|
|
||||||
}
|
|
||||||
6
src/admin/frameworks/materialize.min.js
vendored
6
src/admin/frameworks/materialize.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,32 +0,0 @@
|
|||||||
import { MaterializeConfig } from "./config/materialize.config.js";
|
|
||||||
import { CategoriesComponent } from "./components/categories.component.js";
|
|
||||||
import { ItemsComponent } from "./components/items.component.js";
|
|
||||||
import { LoaderComponent } from "./components/loader.component.js";
|
|
||||||
import { PaginationComponent } from "./components/pagination.component.js";
|
|
||||||
import { SearchComponent } from "./components/search.component.js";
|
|
||||||
import { IconsComponent } from "./components/icons.component.js";
|
|
||||||
import { ItemEditModal } from "./modals/item-edit.modal.js";
|
|
||||||
import { ItemDeleteModal } from "./modals/item-delete.modal.js";
|
|
||||||
import { CategoryEditModal } from "./modals/category-edit.modal";
|
|
||||||
import { ContentComponent } from "./components/content.component.js";
|
|
||||||
import { AdministratorComponent } from "./components/administrator.component.js";
|
|
||||||
import { DashboardComponent } from "./components/dashboard.component.js";
|
|
||||||
|
|
||||||
// Components
|
|
||||||
const loader = new LoaderComponent('loader');
|
|
||||||
const pagination = new PaginationComponent('pagination', {loader});
|
|
||||||
const search = new SearchComponent('search', {loader});
|
|
||||||
const categories = new CategoriesComponent('categories');
|
|
||||||
const icons = new IconsComponent('icons');
|
|
||||||
const items = new ItemsComponent('items', {pagination}, {search});
|
|
||||||
const dashboards = new DashboardComponent('dashboard', {pagination}, {search});
|
|
||||||
const content = new ContentComponent('content');
|
|
||||||
const administrator = new AdministratorComponent('administrator');
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
const itemEdit = new ItemEditModal('editItemModal', 'edit', 'editItemClose');
|
|
||||||
const itemDelete = new ItemDeleteModal('deleteItemModal', 'edit', 'deleteItemClose');
|
|
||||||
const categoryEdit = new CategoryEditModal('editCategoryModal', 'editCategory_select', 'editCategoryClose');
|
|
||||||
|
|
||||||
const materialize = new MaterializeConfig();
|
|
||||||
console.log('Control-Panel v1.0.0');
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { Modal } from "../core/modal";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderIcons } from "../templates/icons.template";
|
|
||||||
|
|
||||||
export class CategoryEditModal extends Modal {
|
|
||||||
constructor(id, open, close) {
|
|
||||||
super(id, open, close);
|
|
||||||
}
|
|
||||||
|
|
||||||
// перепишем метод для перевода ивента open в событие onchange
|
|
||||||
init() {
|
|
||||||
if (this.open) {
|
|
||||||
this.setElems();
|
|
||||||
this.open.addEventListener('change', this.show.bind(this));
|
|
||||||
this.close.addEventListener('click', this.hide.bind(this));
|
|
||||||
this.$el.querySelector('form').addEventListener('submit', this.updateCategory.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$id = this.$el.querySelector('.id');
|
|
||||||
this.$originalTitle = this.$el.querySelector('.originalTitle');
|
|
||||||
this.$title = this.$el.querySelector('.title');
|
|
||||||
this.$type = this.$el.querySelector('.type');
|
|
||||||
this.$format = this.$el.querySelector('.format');
|
|
||||||
this.$img = this.$el.querySelector('.img');
|
|
||||||
}
|
|
||||||
|
|
||||||
async openHandler(e) {
|
|
||||||
const data = JSON.parse(e.target.value),
|
|
||||||
icons = await apiService.getIcons();
|
|
||||||
|
|
||||||
this.$img.innerHTML = `<option value='${data.img}' selected>Оставить прежнюю</option>`;
|
|
||||||
this.$id.value = data.id;
|
|
||||||
this.$originalTitle.value = data.title[0].toUpperCase() + data.title.slice(1);
|
|
||||||
this.$title.value = data.title[0].toUpperCase() + data.title.slice(1);
|
|
||||||
this.$type.value = data.type;
|
|
||||||
this.$format.value = data.format;
|
|
||||||
this.$img.insertAdjacentHTML('beforeend', renderIcons(icons));
|
|
||||||
M.FormSelect.init(this.$img);
|
|
||||||
M.textareaAutoResize(this.$type);
|
|
||||||
M.updateTextFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCategory(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = this.$el.querySelector('form');
|
|
||||||
const categories = await apiService.getCategories();
|
|
||||||
const id = form.elements[0].value.toLowerCase().trim();
|
|
||||||
const index = categories.map((category) => {return category._id}).indexOf(id);
|
|
||||||
index != -1 ? form.submit() : alert('Категории с таким ID не существует.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { Modal } from "../core/modal";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export class ItemDeleteModal extends Modal {
|
|
||||||
constructor(id, open, close) {
|
|
||||||
super(id, open, close);
|
|
||||||
}
|
|
||||||
|
|
||||||
show(e) {
|
|
||||||
if (e.target.id === 'deleteItem') {
|
|
||||||
this.$el.classList.remove('hide');
|
|
||||||
document.body.style.overflowY = 'hidden';
|
|
||||||
this.openHandler(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$content = this.$el.querySelector('.modal-content__form p');
|
|
||||||
this.$button = this.$el.querySelector('.delete-item');
|
|
||||||
}
|
|
||||||
|
|
||||||
openHandler(e) {
|
|
||||||
const data = JSON.parse(e.target.parentNode.parentNode.dataset.info);
|
|
||||||
this.$content.innerHTML = `Вы действительно хотите удалить товар "${data.title}"? Находящиеся в нем аккаунты будут удалены.`;
|
|
||||||
this.$button.onclick = this.deleteHandler.bind(this, data.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteHandler(id) {
|
|
||||||
try {
|
|
||||||
this.$button.classList.add('hide');
|
|
||||||
this.$content.innerHTML = '<div class="progress"><div class="indeterminate"></div></div>'
|
|
||||||
await axios.post('/control-panel/items/delete', {id});
|
|
||||||
alert('Товар был успешно удален.');
|
|
||||||
document.location.reload(true);
|
|
||||||
} catch (error) {
|
|
||||||
alert('Невозможно выполнить запрос.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { Modal } from "../core/modal";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderCategories } from "../templates/categories.template";
|
|
||||||
|
|
||||||
export class ItemEditModal extends Modal {
|
|
||||||
constructor(id, open, close) {
|
|
||||||
super(id, open, close);
|
|
||||||
}
|
|
||||||
|
|
||||||
show(e) {
|
|
||||||
if (e.target.id === 'editItem') {
|
|
||||||
this.$el.classList.remove('hide');
|
|
||||||
document.body.style.overflowY = 'hidden';
|
|
||||||
this.openHandler(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setElems() {
|
|
||||||
this.$id = this.$el.querySelector('.id');
|
|
||||||
this.$title = this.$el.querySelector('.title');
|
|
||||||
this.$price = this.$el.querySelector('.price');
|
|
||||||
this.$description = this.$el.querySelector('.description');
|
|
||||||
this.$category = this.$el.querySelector('.category');
|
|
||||||
this.$data = this.$el.querySelector('.data');
|
|
||||||
}
|
|
||||||
|
|
||||||
async openHandler(e) {
|
|
||||||
const data = JSON.parse(e.target.parentNode.parentNode.dataset.info),
|
|
||||||
categories = await apiService.getCategories();
|
|
||||||
|
|
||||||
this.$category.innerHTML = `<option value='{"title": "${data.category}"}' selected>Оставить прежнюю</option>`;
|
|
||||||
this.$id.value = data.id;
|
|
||||||
this.$title.value = data.title;
|
|
||||||
this.$price.value = data.price;
|
|
||||||
this.$description.value = data.description;
|
|
||||||
this.$category.insertAdjacentHTML('beforeend', renderCategories(categories));
|
|
||||||
this.$data.value = data.data;
|
|
||||||
M.FormSelect.init(this.$category);
|
|
||||||
M.textareaAutoResize(this.$description);
|
|
||||||
M.textareaAutoResize(this.$data);
|
|
||||||
M.updateTextFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
class ApiService {
|
|
||||||
constructor(baseUrl) {
|
|
||||||
this.url = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCategory(category) {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/category/${encodeURIComponent(category)}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCategories() {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/categories`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItems(sorting) {
|
|
||||||
try {
|
|
||||||
if (sorting) {
|
|
||||||
const request = `${this.url}/api/items?sorting=${sorting}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} else {
|
|
||||||
const request = `${this.url}/api/items`;
|
|
||||||
return useRequest(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsOffset(page) {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/items/page/${page}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsQuery(text) {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/items?q=${text}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsOne(category, sorting) {
|
|
||||||
try {
|
|
||||||
if (sorting) {
|
|
||||||
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}?sorting=${sorting}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} else {
|
|
||||||
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}`;
|
|
||||||
return useRequest(request);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsData() {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/items/data`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBuyers() {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/buyers`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBuyersOffset(page) {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/buyers/page/${page}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBillId(bill_id) {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/buyers/${bill_id}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIcons() {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/icons`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function useRequest(request) {
|
|
||||||
const response = await axios.get(request);
|
|
||||||
return await JSON.parse(response.request.response);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const apiService = new ApiService('http://localhost:3000');
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export function renderBuyers(buyers) {
|
|
||||||
let html = '';
|
|
||||||
buyers.forEach(buyer => {
|
|
||||||
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>`;
|
|
||||||
});
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export function renderCategories(categories) {
|
|
||||||
let html = '';
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export function renderIcons(icons) {
|
|
||||||
let html = '';
|
|
||||||
icons.forEach(icon => {
|
|
||||||
html += `<option value='${icon}' data-icon="../img/service-icons/${icon}">${icon}</option>`;
|
|
||||||
});
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
export function renderItems(items, categories, info) {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
if (!items.error) {
|
|
||||||
categories.forEach(category => {
|
|
||||||
items.forEach((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>`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else html += 'Данных нету.';
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class BreadcrumbComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if(this.$el) {
|
|
||||||
const btn = this.$el.querySelector('#breadcrumbBtn');
|
|
||||||
btn.addEventListener('click', buttonHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buttonHandler(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.target.dataset.category) {
|
|
||||||
localStorage.setItem('category', e.target.dataset.category.toLowerCase());
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderItems } from "../templates/items.template";
|
|
||||||
|
|
||||||
export class CategoryComponent extends Component {
|
|
||||||
constructor(id, {items}, {filter}) {
|
|
||||||
super(id);
|
|
||||||
this.items = items;
|
|
||||||
this.filter = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.$el) {
|
|
||||||
this.$el.addEventListener('click', buttonHandler.bind(this));
|
|
||||||
|
|
||||||
if (localStorage.getItem('category')) {
|
|
||||||
localStorage.removeItem('sortByAll', 'true'); // установка значения для компонента Filter
|
|
||||||
|
|
||||||
const category = localStorage.getItem('category');
|
|
||||||
breadcrumbActiveLink.call(this, category); // окрашиваем категорию по нажатию на breadcrumb
|
|
||||||
showItems.call(this, category); // отображем айтемсы если был нажат breadcrumb
|
|
||||||
localStorage.removeItem('category'); // очищаем ключ category чтобы данные не показывались всегда
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showItems(category) {
|
|
||||||
const targetCategory = await apiService.getCategory(category);
|
|
||||||
const fData = await apiService.getItemsOne(category),
|
|
||||||
html = renderItems(targetCategory, fData);
|
|
||||||
|
|
||||||
this.items.onHide();
|
|
||||||
this.items.insertItems(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buttonHandler(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.target.dataset.category && e.target.dataset.category != 'all') { // Выполняем логику если нажали на кнопки кроме "Все товары"
|
|
||||||
localStorage.removeItem('sortByAll'); // установка значения для компонента Filter
|
|
||||||
|
|
||||||
this.filter.clearChecked();
|
|
||||||
this.items.onShow();
|
|
||||||
btnActiveLink(e);
|
|
||||||
|
|
||||||
const category = await apiService.getCategory(e.target.dataset.category),
|
|
||||||
fData = await apiService.getItemsOne(e.target.dataset.category),
|
|
||||||
html = renderItems(category, fData);
|
|
||||||
|
|
||||||
this.items.onHide();
|
|
||||||
this.items.insertItems(html);
|
|
||||||
} else if (e.target.dataset.category === 'all') { // Выполняем логику если нажали на кнопку "Все товары"
|
|
||||||
localStorage.setItem('sortByAll', 'true'); // установка значения для компонента Filter
|
|
||||||
|
|
||||||
this.filter.clearChecked();
|
|
||||||
this.items.onShow();
|
|
||||||
btnActiveLink(e);
|
|
||||||
|
|
||||||
const fData = await apiService.getItems();
|
|
||||||
const categories = await apiService.getCategories();
|
|
||||||
const html = renderItems(categories, fData);
|
|
||||||
|
|
||||||
this.items.onHide();
|
|
||||||
this.items.insertItems(html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Окраска li по нажатию
|
|
||||||
function btnActiveLink(e) {
|
|
||||||
Array.from(e.target.parentNode.parentNode.querySelectorAll('#catLi')).forEach(li => li.classList.remove('active'));
|
|
||||||
e.target.classList.add('active');
|
|
||||||
localStorage.setItem('currentCategory', e.target.dataset.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Окраска li по нажатию на breadcrumb
|
|
||||||
function breadcrumbActiveLink(category) {
|
|
||||||
const links = this.$el.querySelectorAll('#catLi');
|
|
||||||
Array.from(links).forEach(li => li.classList.remove('active'));
|
|
||||||
Array.from(links).forEach(li => li.dataset.category === category ? li.classList.add('active') : false);
|
|
||||||
Array.from(links).forEach(li => li.dataset.category === category ? localStorage.setItem('currentCategory', li.dataset.category) : false);
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { renderItems } from "../templates/items.template";
|
|
||||||
|
|
||||||
export class FilterComponent extends Component {
|
|
||||||
constructor(id, {items}) {
|
|
||||||
super(id);
|
|
||||||
this.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.$el) {
|
|
||||||
this.$el.addEventListener('click', sortByAllHandler.bind(this));
|
|
||||||
this.$el.addEventListener('click', sortByOneHandler.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearChecked() {
|
|
||||||
if (this.$el) {
|
|
||||||
const options = this.$el.querySelectorAll('.filter-options form input');
|
|
||||||
Array.from(options).forEach(option => option.checked = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sortByAllHandler(e) {
|
|
||||||
if (localStorage.getItem('sortByAll')) {
|
|
||||||
if (e.target.id) {
|
|
||||||
const sorting = e.target.id;
|
|
||||||
|
|
||||||
this.items.onShow();
|
|
||||||
const fData = await apiService.getItems(sorting);
|
|
||||||
const categories = await apiService.getCategories();
|
|
||||||
const html = renderItems(categories, fData);
|
|
||||||
|
|
||||||
this.items.onHide();
|
|
||||||
this.items.insertItems(html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sortByOneHandler(e) {
|
|
||||||
if (!localStorage.getItem('sortByAll')) {
|
|
||||||
if (e.target.id) {
|
|
||||||
const sorting = e.target.id;
|
|
||||||
this.items.onShow();
|
|
||||||
|
|
||||||
const category = await apiService.getCategory(localStorage.getItem('currentCategory')),
|
|
||||||
fData = await apiService.getItemsOne(localStorage.getItem('currentCategory'), sorting),
|
|
||||||
html = renderItems(category, fData);
|
|
||||||
|
|
||||||
this.items.onHide();
|
|
||||||
this.items.insertItems(html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { insertCount } from "../other/counter";
|
|
||||||
|
|
||||||
export class HeaderComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.$el) {
|
|
||||||
const currentPage = this.$el.querySelector('#desktop-nav');
|
|
||||||
const pages = Array.from(this.$el.querySelectorAll('#page'));
|
|
||||||
pages.forEach(li => li.dataset.page === currentPage.dataset.current ? li.classList.add('active') : false);
|
|
||||||
// ф-я счетика товаров
|
|
||||||
localStorage.getItem('cart') ? insertCount('counter', JSON.parse(localStorage.getItem('cart'))) : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class InformationComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.$el) {
|
|
||||||
if (!localStorage.getItem('visited')) {
|
|
||||||
this.show();
|
|
||||||
}
|
|
||||||
const btn = this.$el.querySelector('#information-close');
|
|
||||||
btn.addEventListener('click', buttonHandler.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buttonHandler() {
|
|
||||||
localStorage.setItem('visited', JSON.stringify(true));
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
import { apiService } from "../services/api.service";
|
|
||||||
import { insertCount } from "../other/counter";
|
|
||||||
import { renderItems } from "../templates/items.template";
|
|
||||||
|
|
||||||
export class ItemsComponent extends Component {
|
|
||||||
constructor(id, {loader}, {notification}) {
|
|
||||||
super(id);
|
|
||||||
this.loader = loader;
|
|
||||||
this.notification = notification;
|
|
||||||
this.$el ? this.roster = this.$el.querySelector('#roster') : false;
|
|
||||||
if (localStorage.getItem('cart')) {
|
|
||||||
this.cart = JSON.parse(localStorage.getItem('cart'));
|
|
||||||
} else {
|
|
||||||
this.cart = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.$el) {
|
|
||||||
localStorage.setItem('sortByAll', 'true'); // установка значения для компонента Filter
|
|
||||||
this.$el.addEventListener('click', addItem.bind(this));
|
|
||||||
if (!localStorage.getItem('category')) { // Получаем все товары и выводим их если нету ключа category в ls
|
|
||||||
const fData = await apiService.getItems();
|
|
||||||
const categories = await apiService.getCategories();
|
|
||||||
const html = renderItems(categories, fData);
|
|
||||||
this.insertItems(html);
|
|
||||||
this.btnColor();
|
|
||||||
this.loader.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHide() {
|
|
||||||
this.$el.querySelector('#roster').innerHTML = '';
|
|
||||||
this.loader.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
onShow() {
|
|
||||||
this.$el.querySelector('#roster').innerHTML = '';
|
|
||||||
this.loader.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
btnColor() {
|
|
||||||
if (localStorage.getItem('cart')) {
|
|
||||||
// Окраска кнопок айтемов которые находятся в корзине
|
|
||||||
const obj = JSON.parse(localStorage.getItem('cart'));
|
|
||||||
|
|
||||||
obj.forEach(item => {
|
|
||||||
if (this.$el.querySelector(`#rosterButtons[data-id="${item.id}"] #addItem`)) {
|
|
||||||
this.$el.querySelector(`#rosterButtons[data-id="${item.id}"] #addItem`).classList.add('orange');
|
|
||||||
this.$el.querySelector(`#rosterButtons[data-id="${item.id}"] #addItem i`).innerHTML = 'close';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertItems(html) {
|
|
||||||
this.roster.insertAdjacentHTML('afterbegin', html);
|
|
||||||
this.btnColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem(e) {
|
|
||||||
|
|
||||||
if (e.target.localName === 'button' && e.target.id === 'addItem') {
|
|
||||||
const target = e.target;
|
|
||||||
const item = {
|
|
||||||
id: e.target.parentNode.dataset.id,
|
|
||||||
title: e.target.parentNode.dataset.title,
|
|
||||||
count: e.target.parentNode.dataset.count,
|
|
||||||
price: e.target.parentNode.dataset.price
|
|
||||||
}
|
|
||||||
renderObj(item, this, target);
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderObj(obj, context, target) {
|
|
||||||
if (context.cart.length > 0) {
|
|
||||||
let insterted = false;
|
|
||||||
let index;
|
|
||||||
// Проверка на существующий айтем в корзине
|
|
||||||
context.cart.forEach((item, i) => {
|
|
||||||
if (item.id === obj.id) {
|
|
||||||
insterted = true;
|
|
||||||
index = i;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (insterted === false) {
|
|
||||||
context.cart.push(obj)
|
|
||||||
localStorage.setItem('cart', JSON.stringify(context.cart));
|
|
||||||
target.classList.add('orange');
|
|
||||||
target.querySelector('i').innerHTML = 'close';
|
|
||||||
insertCount('counter', context.cart);
|
|
||||||
context.notification.notificate('Товар был успешно добавлен в корзину.', '#26A69A');
|
|
||||||
} else {
|
|
||||||
context.cart.splice(index, 1);
|
|
||||||
localStorage.setItem('cart', JSON.stringify(context.cart));
|
|
||||||
target.classList.remove('orange');
|
|
||||||
target.querySelector('i').innerHTML = 'shopping_cart';
|
|
||||||
insertCount('counter', context.cart);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context.cart.push(obj);
|
|
||||||
localStorage.setItem('cart', JSON.stringify(context.cart));
|
|
||||||
target.classList.add('orange');
|
|
||||||
target.querySelector('i').innerHTML = 'close';
|
|
||||||
insertCount('counter', context.cart);
|
|
||||||
context.notification.notificate('Товар был успешно добавлен в корзину.', '#26A69A');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class LoaderComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { Component } from "../core/component";
|
|
||||||
|
|
||||||
export class NotificationComponent extends Component {
|
|
||||||
constructor(id) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
notificate(text, color) {
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.classList.add('card-panel');
|
|
||||||
card.style.backgroundColor = color;
|
|
||||||
card.innerHTML = `<span class="white-text">${text}</span>`;
|
|
||||||
this.$el.innerHTML += card.outerHTML;
|
|
||||||
this.removeNotificate();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNotificate() {
|
|
||||||
// Полифилл для IE ^9
|
|
||||||
(function() {
|
|
||||||
var arr = [window.Element, window.CharacterData, window.DocumentType];
|
|
||||||
var args = [];
|
|
||||||
|
|
||||||
arr.forEach(function (item) {
|
|
||||||
if (item) {
|
|
||||||
args.push(item.prototype);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(function (arr) {
|
|
||||||
arr.forEach(function (item) {
|
|
||||||
if (item.hasOwnProperty('remove')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.defineProperty(item, 'remove', {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
writable: true,
|
|
||||||
value: function remove() {
|
|
||||||
this.parentNode.removeChild(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})(args);
|
|
||||||
})();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$el.querySelector('.card-panel').remove();
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
const M = require('../frameworks/materialize.min');
|
|
||||||
export class MaterializeConfig {
|
|
||||||
constructor() {
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
M.Sidenav.init(document.querySelectorAll('.sidenav'))
|
|
||||||
M.FormSelect.init(document.querySelectorAll('select'))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
export class Component {
|
|
||||||
constructor(id) {
|
|
||||||
this.$el = document.getElementById(id);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {}
|
|
||||||
|
|
||||||
onShow() {}
|
|
||||||
|
|
||||||
onHide() {}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.$el ? this.$el.classList.add('hide') : false
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
this.$el ? this.$el.classList.remove('hide') : false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
export class Modal {
|
|
||||||
constructor(id, open, close) {
|
|
||||||
this.$el = document.getElementById(id);
|
|
||||||
this.open = document.getElementById(open);
|
|
||||||
this.close = document.getElementById(close);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.open != null) {
|
|
||||||
this.open.addEventListener('click', openHandler.bind(this));
|
|
||||||
this.close.addEventListener('click', closeHandler.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
this.$el.classList.remove('hide');
|
|
||||||
document.body.style.overflowY = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.$el.classList.add('hide');
|
|
||||||
document.body.style.overflowY = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openHandler() {
|
|
||||||
this.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeHandler() {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
6
src/main/frameworks/materialize.min.js
vendored
6
src/main/frameworks/materialize.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,28 +0,0 @@
|
|||||||
import { HeaderComponent } from "./components/header.component.js";
|
|
||||||
import { InformationComponent } from "./components/information.component.js";
|
|
||||||
import { LoaderComponent } from "./components/loader.component.js";
|
|
||||||
import { ItemsComponent } from "./components/items.component.js";
|
|
||||||
import { CategoryComponent } from "./components/category.component.js";
|
|
||||||
import { BreadcrumbComponent } from "./components/breadcrumb.component.js";
|
|
||||||
import { FilterComponent } from "./components/filter.component.js";
|
|
||||||
import { CartModal } from "./modals/cart.modal.js";
|
|
||||||
import { PayModal } from "./modals/pay.modal.js";
|
|
||||||
import { NotificationComponent } from "./components/notification.component.js";
|
|
||||||
import { MaterializeConfig } from "./config/materialize.config.js";
|
|
||||||
|
|
||||||
// Components
|
|
||||||
const header = new HeaderComponent('header');
|
|
||||||
const loader = new LoaderComponent('loader');
|
|
||||||
const information = new InformationComponent('information');
|
|
||||||
const notification = new NotificationComponent('notification');
|
|
||||||
const items = new ItemsComponent('items', {loader}, {notification});
|
|
||||||
const filter = new FilterComponent('filter', {items});
|
|
||||||
const category = new CategoryComponent('category', {items}, {filter});
|
|
||||||
const breadcumb = new BreadcrumbComponent('breadcrumb');
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
const cart = new CartModal('cart', 'cartOpen', 'cartClose', {items});
|
|
||||||
const pay = new PayModal('pay', 'pay-js', 'payClose');
|
|
||||||
|
|
||||||
const materialize = new MaterializeConfig();
|
|
||||||
console.log('Main Page');
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import { Modal } from "../core/modal";
|
|
||||||
import { insertCount } from "../other/counter";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export class CartModal extends Modal {
|
|
||||||
constructor(id, open, close, {items}) {
|
|
||||||
super(id, open, close);
|
|
||||||
this.items = items;
|
|
||||||
if (this.$el) {
|
|
||||||
this.open.addEventListener('click', this.openHandler.bind(this));
|
|
||||||
this.close.addEventListener('click', this.closeHandler.bind(this));
|
|
||||||
this.list = this.$el.querySelector('#modalList');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeHandler() {
|
|
||||||
this.removeItem = null;
|
|
||||||
this.makePay = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
openHandler() {
|
|
||||||
this.removeItem != null ? this.list.addEventListener('click', this.removeItem.bind(this)) : false
|
|
||||||
this.makePay != null ? this.$el.querySelector('form').addEventListener('submit', this.makePay.bind(this)) : false;
|
|
||||||
this.list.innerHTML = '';
|
|
||||||
this.insertItems(this.list);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertItems(list) {
|
|
||||||
if (localStorage.getItem('cart')) {
|
|
||||||
const items = JSON.parse(localStorage.getItem('cart'));
|
|
||||||
|
|
||||||
if (items.length == 0) {
|
|
||||||
list.innerHTML = '<p style="text-align:center;padding:15px;">Вы не добавили ни один товар</p>';
|
|
||||||
this.$el.querySelector('form').classList.add('hide');
|
|
||||||
} else {
|
|
||||||
this.$el.querySelector('form').classList.remove('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach(item => this.list.innerHTML += `<div data-id="${item.id}" data-title="${item.title}" data-price="${item.price}" data-count="1" class="modal-content__item"><div class="modal-content__title"><a href="/product/${item.id}"><b>${item.title}</b></a><br><span class="in-stock">В наличии: ${item.count} (шт)</span></div><div class="modal-content__right"><span id="price">${parseFloat(item.price).toFixed(2)}₽</span></div><div class="modal-content__remove"><span><i data-id="${item.id}" class="material-icons" id="removeItem">close</i></span></div></div>`);
|
|
||||||
insertCount('modalCounter', this.items.cart);
|
|
||||||
insertCount('counter', this.items.cart);
|
|
||||||
this.insertTotalPrice('totalPrice', this.items.cart);
|
|
||||||
} else {
|
|
||||||
list.innerHTML = '<p style="text-align:center;padding:15px;">Вы не добавили ни один товар</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeItem(e) {
|
|
||||||
if (e.target.id === 'removeItem') {
|
|
||||||
const id = e.target.dataset.id;
|
|
||||||
let index;
|
|
||||||
|
|
||||||
this.items.cart.forEach((item, i) => item.id === id ? index = i : false);
|
|
||||||
this.items.cart.splice(index, 1);
|
|
||||||
localStorage.setItem('cart', JSON.stringify(this.items.cart));
|
|
||||||
|
|
||||||
this.list.innerHTML = '';
|
|
||||||
this.insertItems(this.list);
|
|
||||||
this.defaultBtnColor(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertTotalPrice(id, array) {
|
|
||||||
let totalPrice = 0;
|
|
||||||
array.forEach(item => totalPrice += parseFloat(item.price));
|
|
||||||
this.$el.querySelector(`#${id}`).innerHTML = `${totalPrice.toFixed(2)}₽`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async makePay(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const payInfo = {items: this.items.cart.map(item => {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
title: item.title,
|
|
||||||
price: parseFloat(item.price)
|
|
||||||
}
|
|
||||||
})};
|
|
||||||
let totalPrice = 0;
|
|
||||||
payInfo.items.forEach(item => totalPrice += parseFloat(item.price));
|
|
||||||
payInfo.totalPrice = Math.round(totalPrice * 100) / 100;
|
|
||||||
payInfo.payMethod = this.$el.querySelector('.payment-field input').value;
|
|
||||||
payInfo.email = this.$el.querySelector('.email-field input').value;
|
|
||||||
|
|
||||||
const response = await axios.put('/payment', payInfo);
|
|
||||||
response.data.payUrl ? document.location.href = response.data.payUrl : alert(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultBtnColor(id) {
|
|
||||||
if (this.items.roster) {
|
|
||||||
if (this.items.roster.querySelector(`#rosterButtons[data-id="${id}"] #addItem`)) {
|
|
||||||
this.items.roster.querySelector(`#rosterButtons[data-id="${id}"] #addItem`).classList.remove('orange');
|
|
||||||
this.items.roster.querySelector(`#rosterButtons[data-id="${id}"] #addItem i`).innerHTML = 'shopping_cart';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { Modal } from "../core/modal";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export class PayModal extends Modal {
|
|
||||||
constructor (id, open, close) {
|
|
||||||
super(id, open, close);
|
|
||||||
if (this.open != null) {
|
|
||||||
this.open.addEventListener('click', this.openHandler.bind(this));
|
|
||||||
this.close.addEventListener('click', this.closeHandler.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeHandler() {
|
|
||||||
this.$el.querySelector('form').onsubmit = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
openHandler(e) {
|
|
||||||
if (e.target.id === 'payOpen') {
|
|
||||||
const targetInfo = e.target.parentNode.dataset;
|
|
||||||
this.$el.querySelector('.title-field input').value = targetInfo.title;
|
|
||||||
|
|
||||||
this.show();
|
|
||||||
this.$el.querySelector('form').onsubmit = (e) => {
|
|
||||||
this.makePay(e, targetInfo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async makePay(e, info) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const payInfo = {
|
|
||||||
items: [{
|
|
||||||
id: info.id,
|
|
||||||
title: info.title
|
|
||||||
}],
|
|
||||||
totalPrice: parseFloat(info.price)
|
|
||||||
}
|
|
||||||
payInfo.payMethod = this.$el.querySelector('.payment-field input').value;
|
|
||||||
payInfo.email = this.$el.querySelector('.email-field input').value;
|
|
||||||
|
|
||||||
const response = await axios.put('/payment', payInfo);
|
|
||||||
response.data.payUrl ? document.location.href = response.data.payUrl : alert(response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export function insertCount(id, array) {
|
|
||||||
const counter = document.getElementById(id);
|
|
||||||
counter.innerHTML = array.length;
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
class ApiService {
|
|
||||||
constructor(baseUrl) {
|
|
||||||
this.url = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCategory(category) {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/category/${encodeURIComponent(category)}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCategories() {
|
|
||||||
try {
|
|
||||||
const request = `${this.url}/api/categories`;
|
|
||||||
return useRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItems(sorting) {
|
|
||||||
try {
|
|
||||||
if (sorting) {
|
|
||||||
const request = `${this.url}/api/items?sorting=${sorting}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} else {
|
|
||||||
const request = `${this.url}/api/items`;
|
|
||||||
return useRequest(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItemsOne(category, sorting) {
|
|
||||||
try {
|
|
||||||
if (sorting) {
|
|
||||||
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}?sorting=${sorting}`;
|
|
||||||
return useRequest(request);
|
|
||||||
} else {
|
|
||||||
const request = `${this.url}/api/items/${encodeURIComponent(category.toLowerCase())}`;
|
|
||||||
return useRequest(request);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function useRequest(request) {
|
|
||||||
const response = await axios.get(request);
|
|
||||||
return await JSON.parse(response.request.response);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const apiService = new ApiService('http://localhost:3000');
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
export function renderItems(categories, items) {
|
|
||||||
let html = '';
|
|
||||||
try {
|
|
||||||
categories.forEach(category => {
|
|
||||||
html += `<div class="items-category">
|
|
||||||
<h2 class="small-title">${category.title}</h2>
|
|
||||||
</div>`;
|
|
||||||
category.itemsInCategory = 0; // счетчит для отслеживания категорий где нету айтемов
|
|
||||||
try {
|
|
||||||
items.forEach(item => {
|
|
||||||
if (item.category.toLowerCase() === category.title.toLowerCase()) {
|
|
||||||
category.itemsInCategory += 1;
|
|
||||||
html += `
|
|
||||||
<div class="items-list">
|
|
||||||
<div class="items-list__name">
|
|
||||||
<div class="list-icon">
|
|
||||||
<img src="./img/service-icons/${category.img}" alt="#">
|
|
||||||
</div>
|
|
||||||
<div class="list-title">
|
|
||||||
<a href="/product/${item._id}">${item.title}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="items-list__info">
|
|
||||||
<div class="list-date">
|
|
||||||
<small>${diffDates(new Date(), new Date(item.date))}</small>
|
|
||||||
</div>
|
|
||||||
<div class="list-count">
|
|
||||||
<span>${item.count} шт.</span>
|
|
||||||
</div>
|
|
||||||
<div class="list-one">
|
|
||||||
<span>Цена за 1 шт.</span>
|
|
||||||
</div>
|
|
||||||
<div class="list-price">
|
|
||||||
<span>${item.price.toFixed(2)}₽</span>
|
|
||||||
</div>
|
|
||||||
<div id="rosterButtons" data-id="${item._id}" data-title="${item.title}" data-count="${item.count}" data-price="${item.price} class="list-buttons">
|
|
||||||
<button id="payOpen" class="btn-small waves-effect waves-light">Купить</button>
|
|
||||||
<button id="addItem" class="btn-small waves-effect waves-light"><i
|
|
||||||
class="material-icons">shopping_cart</i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
category.itemsInCategory === 0
|
|
||||||
? html += '<div class="items-list"><span>В этой категории товаров пока что нету.</span></div>'
|
|
||||||
: false;
|
|
||||||
} catch {
|
|
||||||
html += '<div class="items-list"><span>В этой категории товаров пока что нету.</span></div>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
html += '<div class="items-list"><span>Добавьте хотя бы одну категорию.</span></div>';
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function diffDates(firstDate, secondDate) {
|
|
||||||
let diff = Math.floor((firstDate - secondDate) / (60 * 60 * 24 * 1000));
|
|
||||||
if (diff <= 0) {
|
|
||||||
return 'добавлено сегодня';
|
|
||||||
}
|
|
||||||
return diff + ' дней назад';
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
admin: ['@babel/polyfill', './src/admin/index.js'],
|
|
||||||
main: ['@babel/polyfill', './src/main/index.js']
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: '[name].js',
|
|
||||||
path: path.join(__dirname, '/dist/public/js/')
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
contentBase: './dist/public/js',
|
|
||||||
proxy: {
|
|
||||||
"/": 'http://localhost:3000'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user