promo!
This commit is contained in:
parent
d801adccaa
commit
acbb58eb0a
1
auto_cart.html
Normal file
1
auto_cart.html
Normal file
File diff suppressed because one or more lines are too long
1
autocart.js
Normal file
1
autocart.js
Normal file
File diff suppressed because one or more lines are too long
16
build.js
Executable file
16
build.js
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import lib from "@minify-html/node";
|
||||
import fs from "fs";
|
||||
import JavascriptObfuscator from "javascript-obfuscator";
|
||||
|
||||
const cfg = {
|
||||
minify_css: true,
|
||||
minify_js: true,
|
||||
keep_comments: false,
|
||||
keep_ssi_comments: false,
|
||||
};
|
||||
|
||||
fs.writeFileSync('autocart.js', JavascriptObfuscator.obfuscate(fs.readFileSync('src/autocart.js').toString()).getObfuscatedCode());
|
||||
fs.writeFileSync('index.html', lib.minify(fs.readFileSync('src/index.html'), cfg));
|
||||
fs.writeFileSync('auto_cart.html', lib.minify(fs.readFileSync('src/auto_cart.html'), cfg));
|
||||
@ -1,8 +1,9 @@
|
||||
/** @file site/dev-server.js */
|
||||
const http = require('http');
|
||||
const http = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const WebSocket = require('ws');
|
||||
import { createHttpsDevServer } from 'easy-https';
|
||||
|
||||
const HTTP_PORT = 9876;
|
||||
const WEBSOCKET_PORT = 9878;
|
||||
@ -60,7 +61,7 @@ function serveStaticPageIfExists(route, res) {
|
||||
* @param {req} req
|
||||
* @param {res} res
|
||||
*/
|
||||
const requestHandler = function (req, res) {
|
||||
const requestHandler = async function (req, res) {
|
||||
const method = req.method.toLowerCase();
|
||||
if (method === 'get') {
|
||||
// No need to ensure the route can't access other local files,
|
||||
@ -74,5 +75,19 @@ const requestHandler = function (req, res) {
|
||||
res.end();
|
||||
};
|
||||
|
||||
const server = http.createServer(requestHandler);
|
||||
server.listen(HTTP_PORT);
|
||||
// const server = http.createServer(requestHandler);
|
||||
// server.listen(HTTP_PORT);
|
||||
|
||||
async function start() {
|
||||
const server = await createHttpsDevServer(
|
||||
requestHandler,
|
||||
{
|
||||
domain: 'local.hvatilo.ru',
|
||||
port: HTTP_PORT,
|
||||
subdomains: ['test'], // will add support for test.my-app.dev
|
||||
openBrowser: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
start();
|
||||
File diff suppressed because one or more lines are too long
1471
package-lock.json
generated
1471
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"start": "./node_modules/http-server/bin/http-server -p 9876 ."
|
||||
"start": "./node_modules/http-server/bin/http-server -p 9876 -S -C local.hvatilo.ru.pem -K local.hvatilo.ru-key.pem -o .",
|
||||
"build": "./build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"http-server": "^14.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@minify-html/node": "^0.18.1",
|
||||
"easy-https": "^1.0.1",
|
||||
"javascript-obfuscator": "^5.5.0",
|
||||
"ws": "^8.20.1"
|
||||
}
|
||||
}
|
||||
|
||||
536
src/auto_cart.html
Normal file
536
src/auto_cart.html
Normal file
@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Автокорзина</title>
|
||||
<style>
|
||||
/* Базовые сбросы */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
||||
body { background-color: #f5f7f8; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; min-height: 100vh; }
|
||||
.container { display: flex; width: 100%; max-width: 1200px; background: #ffffff; border-radius: 24px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04); overflow: hidden; max-height: 90vh; }
|
||||
|
||||
/* --- Колонки --- */
|
||||
.column { padding: 30px; flex: 1; width: 50%; overflow-y: auto;}
|
||||
.column + .column { border-left: 1px solid #f0f0f0; }
|
||||
|
||||
/* --- Левая колонка (Фиксированная высота) --- */
|
||||
.col-left { display: flex; flex-direction: column; height: auto; min-height: 550px; }
|
||||
h1 { font-size: 24px; font-weight: 700; color: #1c1c1c; margin-bottom: 6px; }
|
||||
.subtitle { font-size: 14px; color: #8b8b8b; margin-bottom: 28px; flex-shrink: 0; }
|
||||
|
||||
.cart-summary { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-shrink: 0; }
|
||||
.summary-title { font-size: 18px; font-weight: 600; color: #1c1c1c; }
|
||||
.summary-days { color: #6ba531; }
|
||||
.summary-details { font-size: 14px; color: #8b8b8b; margin-top: 4px; }
|
||||
.reorder-btn { padding: 8px 16px; background: transparent; border: 1px solid #6ba531; color: #6ba531; border-radius: 8px; cursor: pointer; font-size: 14px; transition: 0.2s; flex-shrink: 0; }
|
||||
.reorder-btn:hover { background: #6ba531; color: #fff; }
|
||||
|
||||
/* Список и скролл */
|
||||
.cart-scroll-wrapper { flex: 1; overflow-y: auto; overflow-x: hidden; padding-right: 4px; margin-bottom: 10px; position: relative; transition: max-height 0.3s ease; }
|
||||
.cart-scroll-wrapper::-webkit-scrollbar { width: 6px; }
|
||||
.cart-scroll-wrapper::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
|
||||
.cart-scroll-wrapper::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 4px; }
|
||||
|
||||
.cart-items { list-style: none; }
|
||||
.cart-item { display: flex; align-items: center; padding: 16px 0; border-bottom: 1px solid #f0f0f0; position: relative; transition: opacity 0.4s, transform 0.4s; }
|
||||
.cart-item.replacing { opacity: 0.5; pointer-events: none; }
|
||||
|
||||
.item-image { width: 72px; height: 65px; border-radius: 8px; background-color: #f8f9fa; display: flex; align-items: center; justify-content: center; font-size: 28px; margin-right: 16px; flex-shrink: 0; position: relative; overflow: hidden; }
|
||||
|
||||
/* Лоадер при замене */
|
||||
.cart-item.replacing .item-image::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 30px; height: 30px;
|
||||
border: 3px solid #e1e1e1;
|
||||
border-top: 3px solid #6ba531;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
|
||||
.item-details { flex: 1; }
|
||||
.item-name { font-size: 15px; color: #1c1c1c; margin-bottom: 4px; }
|
||||
.item-price { font-size: 14px; color: #8b8b8b; }
|
||||
|
||||
/* Меню (Три точки) */
|
||||
.menu-btn-wrapper { position: relative; }
|
||||
.menu-btn { background: none; border: none; color: #b0b0b0; cursor: pointer; padding: 4px 8px; font-size: 20px; letter-spacing: 2px; border-radius: 4px; transition: 0.2s; }
|
||||
.menu-btn:hover { background: #f3f4f6; color: #1c1c1c; }
|
||||
.menu-dropdown { position: absolute; right: 0; top: 100%; background: #ffffff; border: 1px solid #eee; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); min-width: 150px; padding: 6px 0; display: none; z-index: 10; margin-top: 4px; }
|
||||
.menu-dropdown.active { display: block; }
|
||||
.menu-dropdown ul { list-style: none; }
|
||||
.menu-dropdown li { padding: 10px 16px; font-size: 14px; color: #1c1c1c; cursor: pointer; transition: 0.2s; }
|
||||
.menu-dropdown li:hover { background: #f3f4f6; }
|
||||
|
||||
.show-more { display: flex; align-items: center; justify-content: center; padding: 20px 0 10px; color: #6ba531; font-size: 14px; cursor: pointer; font-weight: 500; transition: 0.2s; flex-shrink: 0; gap: 4px;}
|
||||
.show-more:hover { opacity: 0.8; }
|
||||
.show-more svg { margin-left: 6px; width: 14px; height: 14px; stroke: #6ba531; }
|
||||
.show-more.hidden { display: none; }
|
||||
|
||||
.cart-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 20px; border-top: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.total-wrap { display: flex; flex-direction: column; align-items: flex-start; }
|
||||
.total-label { font-size: 14px; color: #8b8b8b; }
|
||||
#total-price { font-size: 22px; font-weight: 700; color: #1c1c1c; }
|
||||
.checkout-btn { background-color: #6ba531; color: #ffffff; border: none; padding: 14px 32px; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
|
||||
.checkout-btn:hover { background-color: #5b8f29; }
|
||||
|
||||
/* --- Правая колонка (Аккордеон) --- */
|
||||
.settings-panel { border: 1px solid #ebebeb; border-radius: 16px;}
|
||||
.accordion-item { border-bottom: 1px solid #ebebeb; }
|
||||
.accordion-item:last-child { border-bottom: none; }
|
||||
|
||||
.accordion-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; cursor: pointer; user-select: none; }
|
||||
.header-left { display: flex; align-items: center; }
|
||||
.icon-circle { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; margin-right: 12px; flex-shrink: 0; }
|
||||
.bg-green-light { background-color: #e9f6d9; color: #6ba531; }
|
||||
.bg-gray-light { background-color: #f3f4f6; color: #8b8b8b; }
|
||||
.header-title { font-size: 15px; font-weight: 500; color: #1c1c1c; }
|
||||
.setting-desc { font-size: 14px; color: #8b8b8b; margin-top: 2px; }
|
||||
.arrow-icon { color: #8b8b8b; transition: transform 0.3s ease; }
|
||||
.arrow-icon svg { width: 16px; height: 16px; stroke: #8b8b8b; stroke-width: 2; }
|
||||
|
||||
.accordion-content { max-height: 0; overflow: hidden; transition: max-height 0.4s ease, padding 0.3s ease; padding: 0 24px; }
|
||||
.accordion-item.open .accordion-content { max-height: 500px; padding: 0 24px 20px; }
|
||||
.accordion-item.open .arrow-icon { transform: rotate(180deg); }
|
||||
|
||||
/* Дни */
|
||||
.days-options { display: flex; gap: 12px; }
|
||||
.day-btn { flex: 1; padding: 10px 0; border: 1px solid #e1e1e1; border-radius: 8px; background: #ffffff; font-size: 14px; color: #1c1c1c; cursor: pointer; transition: 0.2s; }
|
||||
.day-btn:hover { border-color: #6ba531; }
|
||||
.day-btn.active { background: #e9f6d9; border-color: #e9f6d9; font-weight: 500; }
|
||||
|
||||
/* Семья (Кто будет есть?) */
|
||||
.family-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.family-member { display: flex; align-items: center; padding: 12px 16px; border: 1px solid #ebebeb; border-radius: 12px; background: #fff; transition: 0.2s; cursor: pointer; }
|
||||
.family-member:hover { border-color: #c0c0c0; box-shadow: 0 2px 8px rgba(0,0,0,0.02); }
|
||||
.family-member .avatar { width: 44px; height: 44px; border-radius: 50%; background: #f8f9fa; display: flex; align-items: center; justify-content: center; font-size: 24px; margin-right: 14px; }
|
||||
.family-member .info { flex: 1; display: flex; flex-direction: column; pointer-events: none; }
|
||||
.family-member .info .name { font-size: 16px; font-weight: 600; color: #1c1c1c; }
|
||||
.family-member .info .age { font-size: 14px; color: #8b8b8b; }
|
||||
.family-member .delete-member { color: #d1d5db; cursor: pointer; font-size: 18px; padding: 0 8px; transition: 0.2s; }
|
||||
.family-member .delete-member:hover { color: #e53935; }
|
||||
|
||||
.add-member-btn { display: flex; align-items: center; justify-content: center; margin-top: 16px; color: #6ba531; font-size: 15px; font-weight: 500; cursor: pointer; transition: 0.2s; }
|
||||
.add-member-btn:hover { opacity: 0.8; }
|
||||
.add-member-btn span { font-size: 20px; margin-left: 8px; font-weight: 300; }
|
||||
|
||||
/* ----- БЛОК: Категории в предпочтениях ----- */
|
||||
.category-selector-block .category-header h3 { font-size: 16px; color: #1c1c1c; margin-bottom: 4px; }
|
||||
.category-selector-block .category-header p { font-size: 14px; color: #8b8b8b; margin-bottom: 16px; }
|
||||
|
||||
.categories-wrapper { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.category-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.category-btn.active {
|
||||
background-color: #134233; /* Темно-зеленый как на скриншоте */
|
||||
color: #ffffff;
|
||||
}
|
||||
.category-btn.inactive {
|
||||
background-color: #f2f9ef; /* Светло-зеленый */
|
||||
color: #1c1c1c;
|
||||
}
|
||||
.category-btn.inactive:hover {
|
||||
background-color: #e4f0de;
|
||||
}
|
||||
|
||||
.toggle-categories-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f2f9ef;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.toggle-categories-btn:hover { background-color: #e4f0de; }
|
||||
.toggle-categories-btn svg { width: 18px; height: 18px; stroke: #1c1c1c; transition: transform 0.3s ease; }
|
||||
.toggle-categories-btn.collapsed svg { transform: rotate(180deg); }
|
||||
|
||||
/* Адрес */
|
||||
.address-input-wrap { position: relative; }
|
||||
.address-input-wrap input { width: 100%; padding: 12px 36px 12px 16px; border: 1px solid #e1e1e1; border-radius: 8px; font-size: 14px; color: #1c1c1c; background: #fafafa; outline: none; }
|
||||
.address-input-wrap .clear-input { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: #b0b0b0; cursor: pointer; font-size: 18px; }
|
||||
|
||||
/* --- Модальное окно (Добавить/Редактировать человека) --- */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(2px); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 20px; }
|
||||
.modal-content { background: #ffffff; border-radius: 24px; width: 100%; max-width: 440px; padding: 24px; box-shadow: 0 8px 30px rgba(0,0,0,0.2); max-height: 95vh; overflow-y: auto;}
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.modal-header h2 { font-size: 20px; font-weight: 600; color: #1c1c1c; }
|
||||
.modal-header .close-modal { font-size: 24px; color: #b0b0b0; cursor: pointer; line-height: 1; }
|
||||
.modal-header .close-modal:hover { color: #1c1c1c; }
|
||||
|
||||
.categories-grid {gap: 4px;display: flex;flex-wrap: wrap;}
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; font-size: 14px; color: #1c1c1c; margin-bottom: 6px; }
|
||||
.form-group input { width: 100%; padding: 10px 12px; border: 1px solid #e1e1e1; border-radius: 8px; font-size: 14px; outline: none; transition: 0.2s; }
|
||||
.form-group input:focus { border-color: #6ba531; }
|
||||
|
||||
.form-row { display: flex; gap: 16px; margin-bottom: 16px; }
|
||||
.form-row .form-group { flex: 1; margin-bottom: 0; }
|
||||
.hint { display: block; font-size: 12px; color: #8b8b8b; margin-top: 4px; }
|
||||
|
||||
.gender-toggles { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.gender-btn { flex: 1; padding: 10px; border: 1px solid #e1e1e1; border-radius: 8px; background: #ffffff; font-size: 14px; cursor: pointer; transition: 0.2s; }
|
||||
.gender-btn.active { background: #e9f6d9; border-color: #e9f6d9; font-weight: 500; }
|
||||
|
||||
/* Внутренние стили для модального окна (Запрещенные / Любимые) */
|
||||
.modal-prefs-grid { display: flex; gap: 16px; margin-bottom: 20px; border-top: 1px solid #f0f0f0; padding-top: 16px; }
|
||||
.modal-prefs-grid .pref-col { flex: 1; position: relative; }
|
||||
.modal-prefs-grid .pref-col-title { display: block; font-size: 14px; font-weight: 600; color: #1c1c1c; margin-bottom: 6px; }
|
||||
.modal-prefs-grid .pref-input { width: 100%; padding: 8px 10px; border: 1px solid #e1e1e1; border-radius: 8px; font-size: 14px; outline: none; margin-bottom: 6px; }
|
||||
.modal-prefs-grid .pref-input:focus { border-color: #6ba531; }
|
||||
.modal-prefs-grid .autocomplete-list { position: absolute; top: 65px; left: 0; width: 100%; background: #fff; border: 1px solid #e1e1e1; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); max-height: 120px; overflow-y: auto; display: none; z-index: 20; }
|
||||
.modal-prefs-grid .autocomplete-list.active { display: block; }
|
||||
.modal-prefs-grid .autocomplete-list div { padding: 8px 12px; cursor: pointer; font-size: 14px; transition: 0.2s; }
|
||||
.modal-prefs-grid .autocomplete-list div:hover { background: #f3f4f6; }
|
||||
.modal-prefs-grid .tags-wrap { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.modal-prefs-grid .tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 6px; font-size: 13px; }
|
||||
.modal-prefs-grid .tag-red { background-color: #fce4ec; color: #c62828; }
|
||||
.modal-prefs-grid .tag-green { background-color: #f0f4c3; color: #33691e; }
|
||||
.modal-prefs-grid .tag-close { margin-left: 6px; cursor: pointer; font-size: 15px; opacity: 0.6; }
|
||||
.modal-prefs-grid .tag-close:hover { opacity: 1; }
|
||||
|
||||
.save-btn { width: 100%; padding: 12px; background: #6ba531; color: #fff; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: 0.2s; }
|
||||
.save-btn:hover { background: #5b8f29; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.container { flex-direction: column; }
|
||||
.column { width: 100%; padding: 20px;}
|
||||
.column + .column { border-left: none; border-top: 1px solid #f0f0f0; }
|
||||
.form-row, .modal-prefs-grid { flex-direction: column; gap: 16px; }
|
||||
.col-left { min-height: 400px; }
|
||||
}
|
||||
|
||||
/* --- Стили для попапа --- */
|
||||
#cookie-popup {
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e9edf0;
|
||||
box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.05), 0px 4px 10px rgba(0, 0, 0, 0.02);
|
||||
padding: 15px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
width: 95%;
|
||||
max-width: 780px;
|
||||
z-index: 9999;
|
||||
|
||||
/* Начальное состояние (скрыто) */
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* Класс для показа попапа */
|
||||
#cookie-popup.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
/* Класс для скрытия (при нажатии) */
|
||||
#cookie-popup.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateX(-50%) translateY(30px);
|
||||
}
|
||||
|
||||
/* Иконка печенья */
|
||||
.cookie-icon-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #edf2f7;
|
||||
box-shadow: inset 0 0 0 1px #ffffff, 0 2px 4px rgba(0,0,0,0.03);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cookie-icon-wrapper img {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Текст */
|
||||
.cookie-text {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
color: #1c3a2e; /* Темный зелено-серый цвет текста */
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.cookie-text .highlight {
|
||||
color: #69a342; /* Зеленый цвет для слова "куки" */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Кнопка */
|
||||
#cookie-accept {
|
||||
flex-shrink: 0;
|
||||
background-color: #d6e6d4; /* Светло-оливковый фон */
|
||||
color: #1c3a2e;
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#cookie-accept:hover {
|
||||
background-color: #c2d6c0;
|
||||
}
|
||||
|
||||
#cookie-accept:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 600px) {
|
||||
#cookie-popup {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
bottom: 15px;
|
||||
text-align: center;
|
||||
border-radius: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cookie-icon-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.cookie-text {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#cookie-accept {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<!-- Левая колонка -->
|
||||
<div class="column col-left" id="app">
|
||||
<h1>Автокорзина</h1>
|
||||
<p class="subtitle">Собираем продукты под ваши настройки</p>
|
||||
|
||||
<div class="cart-summary">
|
||||
<div>
|
||||
<div class="summary-title">Ваша корзина на <span class="summary-days"> - </span></div>
|
||||
<div class="summary-details" id="summary-details">84 товара • 12 020 ₽</div>
|
||||
</div>
|
||||
<button class="reorder-btn">Начать заново</button>
|
||||
</div>
|
||||
|
||||
<div class="cart-scroll-wrapper" id="scroll-wrapper">
|
||||
<ul class="cart-items" id="cart-list"></ul>
|
||||
</div>
|
||||
|
||||
<div class="show-more" id="show-more-btn">
|
||||
Показать ещё <span id="remaining-count">-</span> товаров
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
</div>
|
||||
|
||||
<div class="cart-footer">
|
||||
<div class="total-wrap">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div class="icon-circle bg-green-light" style="margin-right: 12px; width: 40px; height: 40px; font-size: 18px;">🛍️</div>
|
||||
<div>
|
||||
<span class="total-label">Итого</span>
|
||||
<div id="total-price"> - </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="checkout-btn">Оформить заказ</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка -->
|
||||
<div class="column">
|
||||
<h1>Настройте корзину под себя</h1>
|
||||
<p class="subtitle">Чем больше вы расскажете, тем точнее будет подбор</p>
|
||||
|
||||
<div class="settings-panel" id="settings-panel">
|
||||
|
||||
<div class="accordion-item open">
|
||||
<div class="accordion-header">
|
||||
<div class="header-left">
|
||||
<div class="icon-circle bg-green-light">📅</div>
|
||||
<span class="header-title">На сколько дней собираем корзину?</span>
|
||||
</div>
|
||||
<div class="arrow-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<div class="days-options">
|
||||
<button class="day-btn" data-days="1">1</button>
|
||||
<button class="day-btn" data-days="3">3</button>
|
||||
<button class="day-btn" data-days="7">7</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header">
|
||||
<div class="header-left">
|
||||
<div class="icon-circle bg-green-light">👤</div>
|
||||
<div>
|
||||
<div class="header-title">Кто будет есть?</div>
|
||||
<div class="setting-desc" id="family-desc">2 человека</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<div class="family-list" id="family-list">
|
||||
<!-- Семья рендерится через JS -->
|
||||
</div>
|
||||
<div class="add-member-btn" id="addMemberBtn">
|
||||
Добавить человека <span>+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка Предпочтения (Категории) -->
|
||||
<div class="accordion-item" id="prefs-accordion">
|
||||
<div class="accordion-header">
|
||||
<div class="header-left">
|
||||
<div class="icon-circle bg-gray-light">❤️</div>
|
||||
<div>
|
||||
<div class="header-title">Предпочтения</div>
|
||||
<div class="setting-desc" id="prefs-desc">Категории продуктов</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<div class="category-selector-block">
|
||||
<div class="category-header">
|
||||
<p>Выберите группы продуктов, которые хотите видеть в корзине</p>
|
||||
</div>
|
||||
<div class="categories-wrapper">
|
||||
<div class="categories-grid" id="categories-grid"></div>
|
||||
<button class="toggle-categories-btn" id="expand-categories">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно добавления/редактирования человека с НОВЫМИ ПОЛЯМИ -->
|
||||
<div class="modal-overlay" id="personModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">Добавить человека</h2>
|
||||
<span class="close-modal" id="closeModalBtn">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Имя</label>
|
||||
<input type="text" id="inputName" placeholder="Имя">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Пол</label>
|
||||
<div class="gender-toggles" id="genderToggles">
|
||||
<button class="gender-btn" data-gender="f">Женский</button>
|
||||
<button class="gender-btn active" data-gender="m">Мужской</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Возраст</label>
|
||||
<input type="number" id="inputAge" value="30">
|
||||
<span class="hint">от 7 лет</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Вес</label>
|
||||
<input type="number" id="inputWeight" value="87">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Рост</label>
|
||||
<input type="number" id="inputHeight" value="177">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ВОЗВРАЩЁННЫЕ ПОЛЯ: Запрещённые и Любимые -->
|
||||
<div class="modal-prefs-grid">
|
||||
<div class="pref-col">
|
||||
<span class="pref-col-title">Запрещённые</span>
|
||||
<input class="pref-input" type="text" placeholder="Например: лук" data-type="avoid">
|
||||
<div class="autocomplete-list" data-type="avoid"></div>
|
||||
<div class="tags-wrap" data-target="avoid"></div>
|
||||
</div>
|
||||
<div class="pref-col">
|
||||
<span class="pref-col-title">Любимые</span>
|
||||
<input class="pref-input" type="text" data-type="favorite">
|
||||
<div class="autocomplete-list" data-type="favorite"></div>
|
||||
<div class="tags-wrap" data-target="favorite"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="save-btn" id="savePersonBtn">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cookie-popup">
|
||||
<div class="cookie-icon-wrapper">
|
||||
<!-- Замените src на свою картинку печенья -->
|
||||
<img src="https://cdn-icons-png.flaticon.com/512/642/642573.png" alt="Cookie icon">
|
||||
</div>
|
||||
|
||||
<p class="cookie-text">
|
||||
Мы используем <a class="highlight" href="/cookies_and_recommendations.html#c" target="_blank">куки</a> и <a class="highlight" href="/cookies_and_recommendations.html#r" target="_blank">рекомендательные технологии</a>, это помогает
|
||||
улучшать сервис и запоминать ваши настройки
|
||||
</p>
|
||||
|
||||
<button id="cookie-accept">Хорошо</button>
|
||||
</div>
|
||||
<script src="autocart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
703
src/autocart.js
Normal file
703
src/autocart.js
Normal file
@ -0,0 +1,703 @@
|
||||
const host = 'https://hvatilo.ru';
|
||||
// const host = 'https://local.hvatilo.ru:8055';
|
||||
|
||||
// Returns a function, that, when invoked, will only be triggered at most once
|
||||
// during a given window of time. Normally, the throttled function will run
|
||||
// as much as it can, without ever going more than once per `wait` duration;
|
||||
// but if you'd like to disable the execution on the leading edge, pass
|
||||
// `{leading: false}`. To disable execution on the trailing edge, ditto.
|
||||
function throttle(func, wait, options) {
|
||||
var context, args, result;
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
if (!options) options = {};
|
||||
var later = function() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
};
|
||||
return function() {
|
||||
var now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
var remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function refreshToken() {
|
||||
return fetch(host + '/api/guest/refresh_cookie', {method: 'POST', credentials: 'include'}).then(response => {
|
||||
return response.json();
|
||||
})
|
||||
}
|
||||
function initToken() {
|
||||
return fetch(host + '/api/guest/init_cookie?utm_source=hvatilo_promo', {method: 'POST', credentials: 'include'}).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
const requestWithRefresh = async function(url, options) {
|
||||
return fetch(url, {...options, credentials: 'include'})
|
||||
.then(async response => response.json())
|
||||
.then(async data => {
|
||||
if (data.error_message === 'need_refresh') {
|
||||
return refreshToken()
|
||||
.then((r) => {
|
||||
console.log('url', url);
|
||||
return fetch(url, {...options, credentials: 'include'}).then(response => response.json());
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
}
|
||||
|
||||
const replaceProduct = (async function(product_name) {
|
||||
return requestWithRefresh(host + '/api/promo/replace_item', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({product_name})
|
||||
})
|
||||
.then(async () => {
|
||||
updateCart();
|
||||
})
|
||||
})
|
||||
const deleteProduct = (async function(product_name) {
|
||||
return requestWithRefresh(host + '/api/promo/delete_item', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({product_name})
|
||||
})
|
||||
})
|
||||
const updateCart = throttle(async function() {
|
||||
return requestWithRefresh(host + '/api/promo/get_updates', {headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}}).then(d => {
|
||||
if (d.status !== 'complete') {
|
||||
initialData_g.excess = d.payload.excess;
|
||||
initialData_g.precision = d.payload.precision;
|
||||
if (d.status === 'clear') {
|
||||
allProducts = [];
|
||||
visibleProducts = [];
|
||||
|
||||
renderProducts();
|
||||
|
||||
allProducts = d.payload.chunk;
|
||||
visibleProducts = d.payload.chunk;
|
||||
}
|
||||
if (d.status === 'data') {
|
||||
d.payload.chunk.forEach(addedProduct => {
|
||||
let idx = allProducts.findIndex(p => p.name === addedProduct.name)
|
||||
if (idx === -1) {
|
||||
allProducts.push({...addedProduct, image: addedProduct.image?.replace('{SIZE}', '65,fit')});
|
||||
if (visibleProducts.length === allProducts.length - 1) {
|
||||
visibleProducts = allProducts;
|
||||
}
|
||||
} else {
|
||||
allProducts[idx].n += addedProduct.n;
|
||||
if (idx < visibleProducts.length) {
|
||||
visibleProducts[idx] = allProducts[idx];
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
console.log(allProducts.length, visibleProducts.length);
|
||||
if (d.status !== 'waiting') {
|
||||
renderProducts();
|
||||
}
|
||||
updateCart();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
updateCart();
|
||||
})
|
||||
}, 3000);
|
||||
|
||||
const setOption = (key, value) => {
|
||||
return requestWithRefresh(host + '/api/promo/set_option', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({[key]: value})
|
||||
})
|
||||
.then(async d => {
|
||||
wholeRender(await loadInitData());
|
||||
updateCart();
|
||||
})
|
||||
}
|
||||
|
||||
const loadInitData = (async function() {
|
||||
return fetch(host + '/api/promo/get_initial_data', {credentials: 'include'})
|
||||
.then(async response => {
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 401 && data.error_message !== 'need_refresh') {
|
||||
return await initToken()
|
||||
.then((r) => {
|
||||
return fetch(host + '/api/promo/get_initial_data', {credentials: 'include'})
|
||||
.then(r => r.json());
|
||||
});
|
||||
}
|
||||
if (response.status === 403 || data.error_message === 'need_refresh') {
|
||||
return await refreshToken()
|
||||
.then((r) => {
|
||||
return fetch(host + '/api/promo/get_initial_data', {credentials: 'include'})
|
||||
.then(r => r.json());
|
||||
});
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.then((data) => {
|
||||
updateCart();
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
return Promise.reject();
|
||||
})
|
||||
});
|
||||
|
||||
function debounce(func, delay) {
|
||||
let timeoutId;
|
||||
|
||||
return function (...args) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
let allProducts;
|
||||
let visibleProducts;
|
||||
|
||||
function renderProduct(item, index) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'cart-item';
|
||||
const amount = item.unit === 'g' ? item.n + ' г' : item.n + ' шт';
|
||||
li.innerHTML = `
|
||||
<div class="item-image"><img src="${item.image}" alt=""></div>
|
||||
<div class="item-details">
|
||||
<div class="item-name">${item.name}</div>
|
||||
<div class="item-price">${amount} × ${Math.round(item.price * 100) / 100} ₽</div>
|
||||
</div>
|
||||
<div class="menu-btn-wrapper">
|
||||
<button class="menu-btn" data-index="${index}">⋮</button>
|
||||
<div class="menu-dropdown">
|
||||
<ul>
|
||||
<li class="menu-replace">Заменить</li>
|
||||
<li class="menu-remove">Удалить</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
let cartList;
|
||||
let scrollWrapper;
|
||||
let showMoreBtn;
|
||||
let remainingSpan;
|
||||
let initialData_g;
|
||||
|
||||
function renderProducts() {
|
||||
cartList.innerHTML = '';
|
||||
visibleProducts.forEach((item, index) => {
|
||||
cartList.appendChild(renderProduct(item, index));
|
||||
});
|
||||
|
||||
const remaining = allProducts.length - visibleProducts.length;
|
||||
if(remaining > 0) {
|
||||
showMoreBtn.classList.remove('hidden');
|
||||
remainingSpan.textContent = remaining;
|
||||
} else {
|
||||
showMoreBtn.classList.add('hidden');
|
||||
}
|
||||
updateSummaryCount();
|
||||
calculateListHeight();
|
||||
}
|
||||
function updateSummaryCount() {
|
||||
const totalPrice = new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB" }).format(
|
||||
Math.round(allProducts.reduce((sum, item) => sum + item.total_price, 0) * 100) / 100,
|
||||
);
|
||||
const x = Math.round(initialData_g.days * initialData_g.excess + initialData_g.days);
|
||||
document.getElementById('summary-details').innerHTML = `${allProducts.length} ${declension(allProducts.length, ['товар', 'товара', 'товаров'])} • ${totalPrice} ~ с запасами на ${x} ${declension(x, ['день', 'дня', 'дней'])}, точность ${Math.round(initialData_g.precision * 100)/100}%`;
|
||||
document.getElementById('total-price').innerHTML = `${totalPrice}`;
|
||||
if(allProducts.length === 0) {
|
||||
document.getElementById('summary-details').innerHTML = `0 товаров • ${totalPrice} ₽`;
|
||||
}
|
||||
}
|
||||
function calculateListHeight() {
|
||||
const leftCol = document.querySelector('.col-left');
|
||||
if (!leftCol) return;
|
||||
const title = leftCol.querySelector('h1');
|
||||
const subtitle = leftCol.querySelector('.subtitle');
|
||||
const summary = leftCol.querySelector('.cart-summary');
|
||||
const footer = leftCol.querySelector('.cart-footer');
|
||||
const showMore = leftCol.querySelector('.show-more');
|
||||
|
||||
let fixedHeight = 0;
|
||||
if(title) fixedHeight += title.offsetHeight + 6;
|
||||
if(subtitle) fixedHeight += subtitle.offsetHeight + 28;
|
||||
if(summary) fixedHeight += summary.offsetHeight + 24;
|
||||
if(showMore && !showMore.classList.contains('hidden')) fixedHeight += showMore.offsetHeight;
|
||||
if(footer) fixedHeight += footer.offsetHeight + 20;
|
||||
|
||||
const paddingY = 60;
|
||||
const rect = leftCol.getBoundingClientRect();
|
||||
const availHeight = window.innerHeight - rect.top - paddingY - 20;
|
||||
|
||||
const listHeight = availHeight - fixedHeight;
|
||||
if(listHeight > 100) {
|
||||
scrollWrapper.style.maxHeight = `${listHeight}px`;
|
||||
scrollWrapper.style.overflowY = 'auto';
|
||||
}
|
||||
}
|
||||
function declension(number, titles) {
|
||||
const cases = [2, 0, 1, 1, 1, 2];
|
||||
const idx = (number % 100 > 4 && number % 100 < 20)
|
||||
? 2
|
||||
: cases[Math.min(number % 10, 5)];
|
||||
return titles[idx];
|
||||
}
|
||||
|
||||
function wholeRender(initialData) {
|
||||
initialData_g = initialData;
|
||||
document.querySelector('.summary-days').textContent = `${initialData_g.days} ${declension(initialData_g.days, ['день', 'дня', 'дней'])}`;
|
||||
allProducts = (Array.isArray(initialData_g.cart) ? initialData_g.cart : [] ?? []).flat().map(item => {
|
||||
return {...item, image: item.image?.replace('{SIZE}', '65,fit')}
|
||||
});
|
||||
visibleProducts = allProducts.slice(0, visibleProducts ? visibleProducts.length : 5);
|
||||
|
||||
document.querySelectorAll('.days-options .day-btn').forEach(btn => {
|
||||
if (btn.innerText.trim() === `${initialData_g.days}`) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Массив людей теперь содержит поля запрещенных и любимых продуктов
|
||||
console.log(initialData_g);
|
||||
let people = initialData_g.users;
|
||||
|
||||
// --- 1. Рендеринг списка продуктов (Левая часть) ---
|
||||
cartList = document.getElementById('cart-list');
|
||||
scrollWrapper = document.getElementById('scroll-wrapper');
|
||||
showMoreBtn = document.getElementById('show-more-btn');
|
||||
remainingSpan = document.getElementById('remaining-count');
|
||||
|
||||
renderProducts();
|
||||
|
||||
// --- 2. Меню (Заменить и Удалить) ---
|
||||
document.addEventListener('click', function(e) {
|
||||
const menuBtn = e.target.closest('.menu-btn');
|
||||
const menuDropdown = e.target.closest('.menu-dropdown');
|
||||
const isReplace = e.target.closest('.menu-replace');
|
||||
const isRemove = e.target.closest('.menu-remove');
|
||||
const isClearAll = e.target.closest('.reorder-btn');
|
||||
|
||||
if(menuBtn) {
|
||||
const dropdown = menuBtn.nextElementSibling;
|
||||
const isOpen = dropdown.classList.contains('active');
|
||||
document.querySelectorAll('.menu-dropdown.active').forEach(el => el.classList.remove('active'));
|
||||
if(!isOpen) {
|
||||
dropdown.classList.add('active');
|
||||
}
|
||||
e.stopPropagation();
|
||||
} else if(!menuDropdown) {
|
||||
document.querySelectorAll('.menu-dropdown.active').forEach(el => el.classList.remove('active'));
|
||||
}
|
||||
|
||||
if(isReplace) {
|
||||
const wrapper = isReplace.closest('.menu-btn-wrapper');
|
||||
const li = wrapper.closest('.cart-item');
|
||||
li.classList.add('replacing');
|
||||
const index = Array.from(cartList.children).indexOf(li);
|
||||
replaceProduct(allProducts[index].name).then(() => {
|
||||
li.remove();
|
||||
document.querySelectorAll('.menu-dropdown.active').forEach(el => el.classList.remove('active'));
|
||||
visibleProducts.splice(index, 1);
|
||||
allProducts.splice(index, 1);
|
||||
|
||||
if(visibleProducts.length < allProducts.length) {
|
||||
visibleProducts.push(allProducts[visibleProducts.length]);
|
||||
}
|
||||
renderProducts();
|
||||
calculateListHeight();
|
||||
});
|
||||
}
|
||||
|
||||
if(isRemove) {
|
||||
const wrapper = isRemove.closest('.menu-btn-wrapper');
|
||||
const li = wrapper.closest('.cart-item');
|
||||
li.classList.add('replacing');
|
||||
const index = Array.from(cartList.children).indexOf(li);
|
||||
deleteProduct(allProducts[index].name).then(() => {
|
||||
li.remove();
|
||||
document.querySelectorAll('.menu-dropdown.active').forEach(el => el.classList.remove('active'));
|
||||
visibleProducts.splice(index, 1);
|
||||
allProducts.splice(index, 1);
|
||||
|
||||
if(visibleProducts.length < allProducts.length) {
|
||||
visibleProducts.push(allProducts[visibleProducts.length]);
|
||||
}
|
||||
renderProducts();
|
||||
calculateListHeight();
|
||||
})
|
||||
}
|
||||
|
||||
if (isClearAll) {
|
||||
requestWithRefresh(host + '/api/guest/clear_cookie', {method: 'POST'})
|
||||
.then(r => {document.location.reload()});
|
||||
}
|
||||
});
|
||||
|
||||
showMoreBtn.addEventListener('click', () => {
|
||||
while(visibleProducts.length < allProducts.length) {
|
||||
visibleProducts.push(allProducts[visibleProducts.length]);
|
||||
}
|
||||
renderProducts();
|
||||
scrollWrapper.style.maxHeight = '500px';
|
||||
calculateListHeight();
|
||||
});
|
||||
|
||||
// --- 3. Фиксированная высота ---
|
||||
window.addEventListener('resize', calculateListHeight);
|
||||
calculateListHeight();
|
||||
|
||||
// --- 4. Аккордеон (активна только одна вкладка) ---
|
||||
document.querySelectorAll('.accordion-header').forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const item = this.closest('.accordion-item');
|
||||
const isOpen = item.classList.contains('open');
|
||||
document.querySelectorAll('.accordion-item').forEach(el => el.classList.remove('open'));
|
||||
if(!isOpen) {
|
||||
item.classList.add('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- 5. Дни ---
|
||||
document.querySelectorAll('.day-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const parent = this.closest('.days-options');
|
||||
parent.querySelectorAll('.day-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
setOption('days', this.dataset.days);
|
||||
document.querySelector('.summary-days').textContent = `${this.dataset.days} ${declension(this.dataset.days, ['день', 'дня', 'дней'])}`;
|
||||
});
|
||||
});
|
||||
|
||||
// --- 6. Семья с возможностью редактирования ---
|
||||
const familyList = document.getElementById('family-list');
|
||||
const familyDesc = document.getElementById('family-desc');
|
||||
let editingIndex = null;
|
||||
|
||||
function renderPeople() {
|
||||
familyList.innerHTML = '';
|
||||
people.forEach((person, index) => {
|
||||
const avatar = '🥦';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'family-member';
|
||||
div.dataset.index = index;
|
||||
div.innerHTML = `
|
||||
<div class="avatar">${avatar}</div>
|
||||
<div class="info">
|
||||
<div class="name">${person.name}</div>
|
||||
<div class="age">${person.age} лет</div>
|
||||
</div>
|
||||
<div class="delete-member" data-index="${index}">×</div>
|
||||
`;
|
||||
familyList.appendChild(div);
|
||||
});
|
||||
|
||||
const count = people.length;
|
||||
familyDesc.textContent = `${count} ${declension(count, ['человек', 'человека', 'человек'])}`;
|
||||
if(count === 0) familyDesc.textContent = 'Не указаны';
|
||||
|
||||
document.querySelectorAll('.delete-member').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(this.dataset.index);
|
||||
people.splice(idx, 1);
|
||||
renderPeople();
|
||||
setOption('users', people);
|
||||
});
|
||||
});
|
||||
}
|
||||
renderPeople();
|
||||
|
||||
familyList.addEventListener('click', function(e) {
|
||||
const member = e.target.closest('.family-member');
|
||||
if (!member) return;
|
||||
if (e.target.closest('.delete-member')) return;
|
||||
const idx = parseInt(member.dataset.index);
|
||||
openEditModal(idx);
|
||||
});
|
||||
|
||||
// --- 7. Попап (Добавление и Редактирование) + Логика Тегов ---
|
||||
const modal = document.getElementById('personModal');
|
||||
const addBtn = document.getElementById('addMemberBtn');
|
||||
const closeBtn = document.getElementById('closeModalBtn');
|
||||
const saveBtn = document.getElementById('savePersonBtn');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const genderBtns = document.querySelectorAll('.gender-btn');
|
||||
|
||||
const availableTags = initialData_g.available_groups;
|
||||
|
||||
function openEditModal(index = null) {
|
||||
editingIndex = index;
|
||||
|
||||
// Очистка тегов внутри попапа
|
||||
document.querySelectorAll('#personModal .tags-wrap').forEach(el => el.innerHTML = '');
|
||||
|
||||
if (editingIndex !== null) {
|
||||
modalTitle.textContent = 'Редактировать человека';
|
||||
const p = people[editingIndex];
|
||||
document.getElementById('inputName').value = p.name;
|
||||
document.getElementById('inputAge').value = p.age;
|
||||
document.getElementById('inputWeight').value = p.weight;
|
||||
document.getElementById('inputHeight').value = p.height;
|
||||
genderBtns.forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.gender === p.gender);
|
||||
});
|
||||
|
||||
// Рендерим сохраненные теги
|
||||
if(p.avoid) p.avoid.forEach(tag => addTagToModal(tag, 'avoid', false));
|
||||
if(p.favorite) p.favorite.forEach(tag => addTagToModal(tag, 'favorite', false));
|
||||
} else {
|
||||
modalTitle.textContent = 'Добавить человека';
|
||||
document.getElementById('inputName').value = '';
|
||||
document.getElementById('inputAge').value = '';
|
||||
document.getElementById('inputWeight').value = '';
|
||||
document.getElementById('inputHeight').value = '';
|
||||
genderBtns.forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.gender-btn[data-gender="m"]').classList.add('active');
|
||||
}
|
||||
modal.style.display = 'flex';
|
||||
calculateListHeight();
|
||||
}
|
||||
|
||||
addBtn.addEventListener('click', () => openEditModal(null));
|
||||
|
||||
function closeModal() { modal.style.display = 'none'; }
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
genderBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
genderBtns.forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Логика добавления тега в попап
|
||||
function addTagToModal(text, type, saveToPeople = true) {
|
||||
const tagsWrap = document.querySelector(`#personModal .tags-wrap[data-target="${type}"]`);
|
||||
|
||||
// Проверка дубликата в своей группе
|
||||
const existingInGroup = tagsWrap.querySelectorAll('.tag');
|
||||
for(let t of existingInGroup) {
|
||||
if(t.textContent.trim().replace('×','').trim() === text) return;
|
||||
}
|
||||
|
||||
// Взаимоисключение: если тег есть в противоположной группе - удалить его оттуда
|
||||
const oppositeType = type === 'avoid' ? 'favorite' : 'avoid';
|
||||
const oppositeWrap = document.querySelector(`#personModal .tags-wrap[data-target="${oppositeType}"]`);
|
||||
const oppositeTags = oppositeWrap.querySelectorAll('.tag');
|
||||
for(let t of oppositeTags) {
|
||||
if(t.textContent.trim().replace('×','').trim() === text) {
|
||||
t.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const tag = document.createElement('div');
|
||||
tag.className = `tag ${type === 'avoid' ? 'tag-red' : 'tag-green'}`;
|
||||
tag.innerHTML = `${text} <span class="tag-close">×</span>`;
|
||||
tag.querySelector('.tag-close').addEventListener('click', function() {
|
||||
tag.remove();
|
||||
});
|
||||
tagsWrap.appendChild(tag);
|
||||
}
|
||||
|
||||
// Автокомплит для полей в попапе
|
||||
document.querySelectorAll('#personModal .pref-input').forEach(input => {
|
||||
const type = input.dataset.type;
|
||||
const listEl = document.querySelector(`#personModal .autocomplete-list[data-type="${type}"]`);
|
||||
const tagsWrap = document.querySelector(`#personModal .tags-wrap[data-target="${type}"]`);
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
const val = this.value.toLowerCase();
|
||||
listEl.innerHTML = '';
|
||||
|
||||
// Ищем все уже выбранные теги в обеих группах внутри попапа
|
||||
const allSelectedTags = new Set();
|
||||
document.querySelectorAll('#personModal .tags-wrap .tag').forEach(tagEl => {
|
||||
const tagText = tagEl.textContent.trim().replace('×', '').trim();
|
||||
if(tagText) allSelectedTags.add(tagText);
|
||||
});
|
||||
|
||||
if(val.length > 0) {
|
||||
const matches = availableTags.filter(tag =>
|
||||
tag.toLowerCase().includes(val) && !allSelectedTags.has(tag)
|
||||
);
|
||||
|
||||
if(matches.length > 0) {
|
||||
listEl.classList.add('active');
|
||||
matches.forEach(match => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = match;
|
||||
div.addEventListener('click', function() {
|
||||
addTagToModal(match, type);
|
||||
input.value = '';
|
||||
listEl.classList.remove('active');
|
||||
});
|
||||
listEl.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
listEl.classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
listEl.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if(!input.contains(e.target) && !listEl.contains(e.target)) {
|
||||
listEl.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Сохранение пользователя (сбор тегов из DOM попапа)
|
||||
saveBtn.addEventListener('click', () => {
|
||||
const name = document.getElementById('inputName').value.trim();
|
||||
const age = document.getElementById('inputAge').value.trim();
|
||||
const weight = document.getElementById('inputWeight').value.trim();
|
||||
const height = document.getElementById('inputHeight').value.trim();
|
||||
const gender = document.querySelector('.gender-btn.active').dataset.gender;
|
||||
|
||||
if(!name) { alert('Пожалуйста, введите имя.'); return; }
|
||||
if(!age || parseInt(age) < 7) { alert('Возраст должен быть от 7 лет.'); return; }
|
||||
|
||||
// Сбор тегов
|
||||
const forbTags = [];
|
||||
document.querySelector('#personModal .tags-wrap[data-target="avoid"]').querySelectorAll('.tag').forEach(el => {
|
||||
forbTags.push(el.textContent.trim().replace('×', '').trim());
|
||||
});
|
||||
const favTags = [];
|
||||
document.querySelector('#personModal .tags-wrap[data-target="favorite"]').querySelectorAll('.tag').forEach(el => {
|
||||
favTags.push(el.textContent.trim().replace('×', '').trim());
|
||||
});
|
||||
|
||||
const personData = { name, age, weight, height, gender, avoid: forbTags, favorite: favTags };
|
||||
if (editingIndex !== null) {
|
||||
people[editingIndex] = personData;
|
||||
editingIndex = null;
|
||||
} else {
|
||||
people.push(personData);
|
||||
}
|
||||
renderPeople();
|
||||
setOption('users', people);
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// --- 8. Блок: Категории ---
|
||||
const categories = initialData_g.available_groups;
|
||||
const initialSelected = initialData_g.initial_groups;
|
||||
let selectedCategories = [...initialSelected];
|
||||
let isCategoriesExpanded = false;
|
||||
const maxVisibleCategories = 9;
|
||||
const sendUpdatedCategories = debounce(async () => {
|
||||
return setOption('categories', selectedCategories);
|
||||
}, 1500);
|
||||
|
||||
const categoriesGrid = document.getElementById('categories-grid');
|
||||
const toggleBtn = document.getElementById('expand-categories');
|
||||
|
||||
function renderCategories() {
|
||||
categoriesGrid.innerHTML = '';
|
||||
const dataToShow = isCategoriesExpanded ? categories : categories.slice(0, maxVisibleCategories);
|
||||
|
||||
dataToShow.forEach(cat => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `category-btn ${selectedCategories.includes(cat) ? 'active' : 'inactive'}`;
|
||||
btn.textContent = cat;
|
||||
btn.dataset.category = cat;
|
||||
btn.addEventListener('click', function() {
|
||||
const catName = this.dataset.category;
|
||||
if (selectedCategories.includes(catName)) {
|
||||
selectedCategories = selectedCategories.filter(c => c !== catName);
|
||||
} else {
|
||||
selectedCategories.push(catName);
|
||||
}
|
||||
renderCategories();
|
||||
sendUpdatedCategories();
|
||||
});
|
||||
categoriesGrid.appendChild(btn);
|
||||
});
|
||||
|
||||
if (isCategoriesExpanded) {
|
||||
toggleBtn.classList.remove('collapsed');
|
||||
} else {
|
||||
toggleBtn.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
isCategoriesExpanded = !isCategoriesExpanded;
|
||||
renderCategories();
|
||||
});
|
||||
renderCategories();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
wholeRender(await loadInitData());
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const popup = document.getElementById('cookie-popup');
|
||||
const acceptBtn = document.getElementById('cookie-accept');
|
||||
|
||||
// Проверяем, согласился ли пользователь ранее
|
||||
if (!localStorage.getItem('cookie_accepted')) {
|
||||
// Небольшая задержка для красоты появления
|
||||
setTimeout(() => {
|
||||
popup.classList.add('show');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Обработчик нажатия на кнопку "Хорошо"
|
||||
acceptBtn.addEventListener('click', function() {
|
||||
// Сохраняем согласие в браузер
|
||||
localStorage.setItem('cookie_accepted', 'true');
|
||||
|
||||
// Запускаем анимацию исчезновения
|
||||
popup.classList.remove('show');
|
||||
popup.classList.add('hide');
|
||||
|
||||
// Полностью удаляем из DOM после завершения анимации
|
||||
setTimeout(() => {
|
||||
popup.style.display = 'none';
|
||||
}, 400);
|
||||
});
|
||||
});
|
||||
13
src/cookies_and_recommendations.html
Normal file
13
src/cookies_and_recommendations.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Рекомендательные технологии и куки</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -232,9 +232,10 @@
|
||||
#page_autobasket_images div {gap: 8px;}
|
||||
#page_autobasket_images div div {gap: 2px;}
|
||||
#page_autobasket_images div img {max-width: 100% !important;}
|
||||
#page_autobasket_content_description {max-width: 1500px;}
|
||||
p.description_large {font-size: 16px;}
|
||||
#page_autobasket_content_description h2 {padding-bottom: 24px;}
|
||||
#page_autobasket_content_description p {font-size: 16px;margin: 0;}
|
||||
p.description_large {font-size: 16px;}
|
||||
#page_how_it_works_info {height: auto;}
|
||||
#page_how_it_works_steps {font-size: 14px;padding:0;align-items: initial;margin-top: 20px;}
|
||||
#page_how_it_works_steps p {display: flex;align-content: center;align-items: center;width: 100%;justify-content: center;}
|
||||
Loading…
Reference in New Issue
Block a user