|
|
from flask import Flask, render_template_string, request, redirect, url_for, flash, send_file |
|
|
import json |
|
|
import os |
|
|
import logging |
|
|
import threading |
|
|
import time |
|
|
from datetime import datetime |
|
|
from huggingface_hub import HfApi, hf_hub_download |
|
|
from huggingface_hub.utils import RepositoryNotFoundError |
|
|
from werkzeug.utils import secure_filename |
|
|
import uuid |
|
|
import html |
|
|
import random |
|
|
|
|
|
app = Flask(__name__) |
|
|
app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart") |
|
|
DATA_FILE = 'data_zzirix.json' |
|
|
|
|
|
REPO_ID = "Kgshop/clients" |
|
|
HF_TOKEN_WRITE = os.getenv("HF_TOKEN") |
|
|
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE |
|
|
|
|
|
LOGO_URL = "https://huggingface.co/spaces/Kgshop/Zzirixadm/resolve/main/Picsart_25-03-20_15-38-36-600.jpg" |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
|
|
|
def initialize_data_structure(data): |
|
|
if not isinstance(data, dict): |
|
|
data = {'categories': [], 'products': [], 'orders': {}} |
|
|
|
|
|
data.setdefault('categories', []) |
|
|
data.setdefault('products', []) |
|
|
data.setdefault('orders', {}) |
|
|
|
|
|
if any(p.get('category') == 'Без категории' for p in data['products']) and 'Без категории' not in data['categories']: |
|
|
data['categories'].append('Без категории') |
|
|
|
|
|
for product in data['products']: |
|
|
if 'id' not in product: |
|
|
product['id'] = str(uuid.uuid4()) |
|
|
|
|
|
product.setdefault('name', 'Без названия') |
|
|
product.setdefault('description', '') |
|
|
product.setdefault('category', 'Без категории') |
|
|
product.setdefault('price', 0.0) |
|
|
product.setdefault('colors', []) |
|
|
product.setdefault('models', []) |
|
|
product.setdefault('photos', []) |
|
|
product.setdefault('in_stock', True) |
|
|
product.setdefault('is_top', False) |
|
|
|
|
|
if not product['photos'] and 'media' in product and product['media']: |
|
|
product['photos'] = [m['filename'] for m in product['media'] if m['type'] == 'photo'] |
|
|
|
|
|
if 'media' in product: |
|
|
del product['media'] |
|
|
|
|
|
data['categories'] = sorted(list(set(data['categories'])), key=lambda x: (x != 'Без категории', x)) |
|
|
|
|
|
return data |
|
|
|
|
|
def load_data(): |
|
|
try: |
|
|
if not os.path.exists(DATA_FILE) or os.path.getsize(DATA_FILE) == 0: |
|
|
logging.info(f"{DATA_FILE} не найден или пуст, попытка загрузки с HF.") |
|
|
download_db_from_hf() |
|
|
|
|
|
if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0: |
|
|
with open(DATA_FILE, 'r', encoding='utf-8') as file: |
|
|
data = json.load(file) |
|
|
logging.info("Данные успешно загружены из JSON.") |
|
|
else: |
|
|
data = {'products': [], 'categories': [], 'orders': {}} |
|
|
logging.warning("Файл базы данных пуст или не существует после всех попыток, создается пустая структура.") |
|
|
|
|
|
return initialize_data_structure(data) |
|
|
except (json.JSONDecodeError, FileNotFoundError) as e: |
|
|
logging.error(f"Ошибка при чтении {DATA_FILE}: {e}. Создается пустая структура.") |
|
|
return initialize_data_structure({}) |
|
|
except RepositoryNotFoundError as e: |
|
|
logging.error(f"Репозиторий HF не найден: {e}. Создается локальная база данных.") |
|
|
return initialize_data_structure({}) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при загрузке данных: {e}") |
|
|
return initialize_data_structure({}) |
|
|
|
|
|
def save_data(data): |
|
|
try: |
|
|
temp_file = DATA_FILE + '.tmp' |
|
|
with open(temp_file, 'w', encoding='utf-8') as file: |
|
|
json.dump(data, file, ensure_ascii=False, indent=4) |
|
|
os.replace(temp_file, DATA_FILE) |
|
|
upload_db_to_hf() |
|
|
logging.info("Данные сохранены и выгружены в HF") |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при сохранении данных: {e}") |
|
|
if os.path.exists(temp_file): |
|
|
os.remove(temp_file) |
|
|
raise |
|
|
|
|
|
def upload_db_to_hf(): |
|
|
if not HF_TOKEN_WRITE: |
|
|
logging.warning("HF_TOKEN_WRITE не установлен. Пропуск выгрузки.") |
|
|
return |
|
|
try: |
|
|
api = HfApi() |
|
|
api.upload_file( |
|
|
path_or_fileobj=DATA_FILE, |
|
|
path_in_repo=DATA_FILE, |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Резервная копия {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" |
|
|
) |
|
|
logging.info("База данных выгружена в Hugging Face") |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при выгрузке базы данных: {e}") |
|
|
|
|
|
def download_db_from_hf(): |
|
|
if not HF_TOKEN_READ: |
|
|
logging.warning("HF_TOKEN_READ не установлен. Пропуск загрузки.") |
|
|
if not os.path.exists(DATA_FILE): |
|
|
with open(DATA_FILE, 'w', encoding='utf-8') as f: |
|
|
json.dump(initialize_data_structure({}), f) |
|
|
return |
|
|
try: |
|
|
hf_hub_download( |
|
|
repo_id=REPO_ID, |
|
|
filename=DATA_FILE, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_READ, |
|
|
local_dir=".", |
|
|
local_dir_use_symlinks=False, |
|
|
force_download=True |
|
|
) |
|
|
logging.info("База данных загружена с Hugging Face") |
|
|
except RepositoryNotFoundError: |
|
|
logging.error(f"Репозиторий {REPO_ID} не найден. Пропускаем загрузку.") |
|
|
if not os.path.exists(DATA_FILE): |
|
|
logging.info("Создается пустой файл базы данных, так как загрузка не удалась и файл не существует.") |
|
|
with open(DATA_FILE, 'w', encoding='utf-8') as f: |
|
|
json.dump(initialize_data_structure({}), f) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при загрузке базы данных: {e}") |
|
|
if not os.path.exists(DATA_FILE): |
|
|
logging.info("Создается пустой файл базы данных, так как загрузка не удалась и файл не существует.") |
|
|
with open(DATA_FILE, 'w', encoding='utf-8') as f: |
|
|
json.dump(initialize_data_structure({}), f) |
|
|
|
|
|
def periodic_backup(): |
|
|
while True: |
|
|
time.sleep(3600) |
|
|
logging.info("Запуск периодического резервного копирования.") |
|
|
try: |
|
|
data = load_data() |
|
|
save_data(data) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка во время периодического резервного копирования: {e}") |
|
|
|
|
|
def allowed_file(filename): |
|
|
return '.' in filename and \ |
|
|
filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'webp', 'mp4', 'mov', 'webm'} |
|
|
|
|
|
BASE_STYLE = ''' |
|
|
:root { |
|
|
--primary-color: #3B82F6; |
|
|
--primary-dark-color: #2563eb; |
|
|
--accent-color: #10b981; |
|
|
--accent-dark-color: #059669; |
|
|
--danger-color: #ef4444; |
|
|
--danger-dark-color: #dc2626; |
|
|
--background-light: linear-gradient(135deg, #f0f2f5, #e0e5ec); |
|
|
--background-dark: linear-gradient(135deg, #1f2937, #374151); |
|
|
--card-background-light: #ffffff; |
|
|
--card-background-dark: #2d3748; |
|
|
--text-color-light: #2d3748; |
|
|
--text-color-dark: #e2e8f0; |
|
|
--secondary-text-color-light: #718096; |
|
|
--secondary-text-color-dark: #a0aec0; |
|
|
--border-color-light: #e2e8f0; |
|
|
--border-color-dark: #4a5568; |
|
|
--shadow-light: 0 8px 25px rgba(0, 0, 0, 0.1); |
|
|
--shadow-hover-light: 0 12px 35px rgba(0, 0, 0, 0.18); |
|
|
--shadow-dark: 0 8px 25px rgba(0, 0, 0, 0.35); |
|
|
--shadow-hover-dark: 0 12px 35px rgba(0, 0, 0, 0.5); |
|
|
--border-radius-large: 22px; |
|
|
--border-radius-medium: 12px; |
|
|
--border-radius-small: 8px; |
|
|
} |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
body { |
|
|
font-family: 'Roboto', sans-serif; |
|
|
background: var(--background-light); |
|
|
color: var(--text-color-light); |
|
|
line-height: 1.6; |
|
|
transition: background 0.3s, color 0.3s; |
|
|
min-height: 100vh; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
body.dark-mode { |
|
|
background: var(--background-dark); |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
.container { |
|
|
max-width: 1400px; |
|
|
margin: 0 auto; |
|
|
padding: 30px; |
|
|
flex-grow: 1; |
|
|
} |
|
|
.header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
padding: 20px 30px; |
|
|
border-bottom: none; |
|
|
margin-bottom: 30px; |
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05); |
|
|
border-radius: var(--border-radius-large); |
|
|
background: var(--card-background-light); |
|
|
} |
|
|
body.dark-mode .header { |
|
|
border-bottom-color: var(--border-color-dark); |
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2); |
|
|
background: var(--card-background-dark); |
|
|
} |
|
|
.header-info { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
} |
|
|
.header-logo { |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
border-radius: 50%; |
|
|
object-fit: cover; |
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); |
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
|
|
margin-right: 15px; |
|
|
} |
|
|
.header-logo:hover { |
|
|
transform: scale(1.08); |
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); |
|
|
} |
|
|
.header h1 { |
|
|
font-size: 2rem; |
|
|
font-weight: 800; |
|
|
color: var(--primary-color); |
|
|
} |
|
|
.theme-toggle { |
|
|
background: none; |
|
|
border: none; |
|
|
font-size: 1.8rem; |
|
|
cursor: pointer; |
|
|
color: var(--secondary-text-color-light); |
|
|
transition: color 0.3s ease, transform 0.2s ease; |
|
|
padding: 5px; |
|
|
} |
|
|
body.dark-mode .theme-toggle { |
|
|
color: var(--secondary-text-color-dark); |
|
|
} |
|
|
.theme-toggle:hover { |
|
|
color: var(--accent-color); |
|
|
transform: rotate(30deg); |
|
|
} |
|
|
|
|
|
.flash { |
|
|
padding: 18px; |
|
|
margin-bottom: 25px; |
|
|
border-radius: var(--border-radius-medium); |
|
|
font-weight: 500; |
|
|
border: 1px solid transparent; |
|
|
font-size: 1.1rem; |
|
|
} |
|
|
.flash.success { |
|
|
background-color: var(--accent-color); |
|
|
color: white; |
|
|
border-color: var(--accent-dark-color); |
|
|
} |
|
|
.flash.error { |
|
|
background-color: var(--danger-color); |
|
|
color: white; |
|
|
border-color: var(--danger-dark-color); |
|
|
} |
|
|
|
|
|
.filters-container { |
|
|
margin: 30px 0; |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 15px; |
|
|
justify-content: center; |
|
|
} |
|
|
.search-container { |
|
|
margin: 25px 0; |
|
|
text-align: center; |
|
|
} |
|
|
#search-input { |
|
|
width: 90%; |
|
|
max-width: 600px; |
|
|
padding: 14px 22px; |
|
|
font-size: 1rem; |
|
|
border: none; |
|
|
border-radius: var(--border-radius-medium); |
|
|
outline: none; |
|
|
box-shadow: var(--shadow-light); |
|
|
transition: all 0.3s ease; |
|
|
background-color: var(--card-background-light); |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode #search-input { |
|
|
border-color: var(--border-color-dark); |
|
|
background-color: var(--card-background-dark); |
|
|
color: var(--text-color-dark); |
|
|
box-shadow: var(--shadow-dark); |
|
|
} |
|
|
#search-input:focus { |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 10px rgba(59, 130, 246, 0.4); |
|
|
} |
|
|
.category-filter { |
|
|
padding: 12px 25px; |
|
|
border: none; |
|
|
border-radius: var(--border-radius-medium); |
|
|
background-color: var(--card-background-light); |
|
|
cursor: pointer; |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
font-size: 1rem; |
|
|
font-weight: 500; |
|
|
color: var(--text-color-light); |
|
|
box-shadow: var(--shadow-light); |
|
|
} |
|
|
body.dark-mode .category-filter { |
|
|
border-color: var(--border-color-dark); |
|
|
background-color: var(--card-background-dark); |
|
|
color: var(--text-color-dark); |
|
|
box-shadow: var(--shadow-dark); |
|
|
} |
|
|
.category-filter.active, .category-filter:hover { |
|
|
background-color: var(--primary-color); |
|
|
color: white; |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: var(--shadow-hover-light); |
|
|
} |
|
|
body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { |
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: var(--shadow-hover-dark); |
|
|
} |
|
|
|
|
|
.products-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
|
|
gap: 25px; |
|
|
padding: 15px; |
|
|
} |
|
|
.product { |
|
|
background: var(--card-background-light); |
|
|
border-radius: var(--border-radius-large); |
|
|
padding: 20px; |
|
|
box-shadow: var(--shadow-light); |
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; |
|
|
overflow: hidden; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: space-between; |
|
|
} |
|
|
body.dark-mode .product { |
|
|
background: var(--card-background-dark); |
|
|
box-shadow: var(--shadow-dark); |
|
|
} |
|
|
.product:hover { |
|
|
transform: translateY(-10px) scale(1.03); |
|
|
box-shadow: var(--shadow-hover-light); |
|
|
} |
|
|
body.dark-mode .product:hover { |
|
|
box-shadow: var(--shadow-hover-dark); |
|
|
} |
|
|
.product-image { |
|
|
width: 100%; |
|
|
aspect-ratio: 1; |
|
|
background-color: #f7f7f7; |
|
|
border-radius: var(--border-radius-medium); |
|
|
overflow: hidden; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
margin-bottom: 15px; |
|
|
cursor: pointer; |
|
|
} |
|
|
body.dark-mode .product-image { |
|
|
background-color: #343e4d; |
|
|
} |
|
|
.product-image img, .product-image video { |
|
|
max-width: 100%; |
|
|
max-height: 100%; |
|
|
object-fit: contain; |
|
|
transition: transform 0.3s ease; |
|
|
border-radius: var(--border-radius-medium); |
|
|
} |
|
|
.product-image img:hover, .product-image video:hover { |
|
|
transform: scale(1.05); |
|
|
} |
|
|
.product h2 { |
|
|
font-size: 1.2rem; |
|
|
font-weight: 700; |
|
|
margin: 8px 0; |
|
|
text-align: center; |
|
|
white-space: nowrap; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode .product h2 { |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
.product-price { |
|
|
font-size: 1.3rem; |
|
|
color: var(--danger-color); |
|
|
font-weight: 700; |
|
|
text-align: center; |
|
|
margin: 8px 0 12px; |
|
|
} |
|
|
.product-description { |
|
|
font-size: 0.9rem; |
|
|
color: var(--secondary-text-color-light); |
|
|
text-align: center; |
|
|
margin-bottom: 20px; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
white-space: nowrap; |
|
|
} |
|
|
body.dark-mode .product-description { |
|
|
color: var(--secondary-text-color-dark); |
|
|
} |
|
|
.product-actions { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 10px; |
|
|
margin-top: auto; |
|
|
} |
|
|
.product-button { |
|
|
display: block; |
|
|
width: 100%; |
|
|
padding: 12px; |
|
|
border: none; |
|
|
border-radius: var(--border-radius-medium); |
|
|
background-color: var(--primary-color); |
|
|
color: white; |
|
|
font-size: 1rem; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
text-align: center; |
|
|
text-decoration: none; |
|
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1); |
|
|
} |
|
|
.product-button:hover { |
|
|
background-color: var(--primary-dark-color); |
|
|
box-shadow: 0 6px 15px rgba(59, 130, 246, 0.4); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
.add-to-cart { |
|
|
background-color: var(--accent-color); |
|
|
box-shadow: 0 3px 10px rgba(16, 185, 129, 0.2); |
|
|
} |
|
|
.add-to-cart:hover { |
|
|
background-color: var(--accent-dark-color); |
|
|
box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); |
|
|
} |
|
|
#cart-button { |
|
|
position: fixed; |
|
|
bottom: 30px; |
|
|
right: 30px; |
|
|
background-color: var(--danger-color); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 50%; |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
font-size: 1.5rem; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3); |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
z-index: 1000; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
#cart-button:hover { |
|
|
background-color: var(--danger-dark-color); |
|
|
transform: translateY(-5px) scale(1.08); |
|
|
box-shadow: 0 12px 35px rgba(239, 68, 68, 0.5); |
|
|
} |
|
|
.cart-count { |
|
|
position: absolute; |
|
|
top: -5px; |
|
|
right: -5px; |
|
|
background-color: var(--accent-color); |
|
|
color: white; |
|
|
border-radius: 50%; |
|
|
padding: 4px 8px; |
|
|
font-size: 0.8rem; |
|
|
min-width: 22px; |
|
|
text-align: center; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.modal { |
|
|
display: none; |
|
|
position: fixed; |
|
|
z-index: 1001; |
|
|
left: 0; |
|
|
top: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0,0,0,0.6); |
|
|
backdrop-filter: blur(8px); |
|
|
overflow-y: auto; |
|
|
padding: 20px; |
|
|
} |
|
|
.modal-content { |
|
|
background: var(--card-background-light); |
|
|
margin: 50px auto; |
|
|
padding: 40px; |
|
|
border-radius: var(--border-radius-large); |
|
|
width: 95%; |
|
|
max-width: 750px; |
|
|
box-shadow: var(--shadow-hover-light); |
|
|
animation: fadeInScale 0.3s ease-out; |
|
|
max-height: 90vh; |
|
|
overflow-y: auto; |
|
|
-webkit-overflow-scrolling: touch; |
|
|
position: relative; |
|
|
} |
|
|
body.dark-mode .modal-content { |
|
|
background: var(--card-background-dark); |
|
|
color: var(--text-color-dark); |
|
|
box-shadow: var(--shadow-hover-dark); |
|
|
} |
|
|
@keyframes fadeInScale { |
|
|
from { opacity: 0; transform: translateY(-30px) scale(0.95); } |
|
|
to { opacity: 1; transform: translateY(0) scale(1); } |
|
|
} |
|
|
.close { |
|
|
position: absolute; |
|
|
top: 20px; |
|
|
right: 25px; |
|
|
font-size: 2rem; |
|
|
color: var(--secondary-text-color-light); |
|
|
cursor: pointer; |
|
|
transition: color 0.3s, transform 0.2s; |
|
|
} |
|
|
.close:hover { |
|
|
color: var(--danger-color); |
|
|
transform: rotate(90deg); |
|
|
} |
|
|
body.dark-mode .close { |
|
|
color: var(--secondary-text-color-dark); |
|
|
} |
|
|
body.dark-mode .close:hover { |
|
|
color: var(--danger-color); |
|
|
} |
|
|
|
|
|
.modal h2 { |
|
|
font-size: 1.8rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 25px; |
|
|
text-align: center; |
|
|
} |
|
|
.cart-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 18px 0; |
|
|
border-bottom: 1px solid var(--border-color-light); |
|
|
} |
|
|
body.dark-mode .cart-item { |
|
|
border-bottom-color: var(--border-color-dark); |
|
|
} |
|
|
.cart-item:last-child { |
|
|
border-bottom: none; |
|
|
} |
|
|
.cart-item img { |
|
|
width: 70px; |
|
|
height: 70px; |
|
|
object-fit: contain; |
|
|
border-radius: var(--border-radius-small); |
|
|
margin-right: 20px; |
|
|
background-color: #f7f7f7; |
|
|
} |
|
|
body.dark-mode .cart-item img { |
|
|
background-color: #343e4d; |
|
|
} |
|
|
.cart-item-details { |
|
|
flex-grow: 1; |
|
|
} |
|
|
.cart-item-details strong { |
|
|
font-size: 1.2rem; |
|
|
font-weight: 600; |
|
|
} |
|
|
.cart-item-details p { |
|
|
font-size: 0.95rem; |
|
|
color: var(--secondary-text-color-light); |
|
|
} |
|
|
body.dark-mode .cart-item-details p { |
|
|
color: var(--secondary-text-color-dark); |
|
|
} |
|
|
.cart-item-total { |
|
|
font-size: 1.1rem; |
|
|
font-weight: 700; |
|
|
color: var(--danger-color); |
|
|
} |
|
|
.quantity-input, .color-select, .model-select { |
|
|
width: 100%; |
|
|
padding: 12px; |
|
|
border: 1px solid var(--border-color-light); |
|
|
border-radius: var(--border-radius-medium); |
|
|
font-size: 1rem; |
|
|
margin: 10px 0; |
|
|
background-color: var(--card-background-light); |
|
|
color: var(--text-color-light); |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); |
|
|
} |
|
|
body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .model-select { |
|
|
border-color: var(--border-color-dark); |
|
|
background-color: var(--card-background-dark); |
|
|
color: var(--text-color-dark); |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2); |
|
|
} |
|
|
.modal-buttons { |
|
|
margin-top: 30px; |
|
|
display: flex; |
|
|
justify-content: flex-end; |
|
|
gap: 15px; |
|
|
} |
|
|
.modal-buttons .product-button { |
|
|
width: auto; |
|
|
padding: 12px 25px; |
|
|
} |
|
|
.clear-cart { |
|
|
background-color: var(--danger-color); |
|
|
} |
|
|
.clear-cart:hover { |
|
|
background-color: var(--danger-dark-color); |
|
|
box-shadow: 0 6px 15px rgba(239, 68, 68, 0.4); |
|
|
} |
|
|
.order-button { |
|
|
background-color: var(--accent-color); |
|
|
} |
|
|
.order-button:hover { |
|
|
background-color: var(--accent-dark-color); |
|
|
box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); |
|
|
} |
|
|
|
|
|
.swiper-container { |
|
|
max-width: 500px; |
|
|
margin: 0 auto 30px; |
|
|
border-radius: var(--border-radius-large); |
|
|
overflow: hidden; |
|
|
box-shadow: var(--shadow-light); |
|
|
} |
|
|
body.dark-mode .swiper-container { |
|
|
box-shadow: var(--shadow-dark); |
|
|
} |
|
|
.swiper-slide { |
|
|
background-color: #f7f7f7; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
min-height: 300px; |
|
|
} |
|
|
body.dark-mode .swiper-slide { |
|
|
background-color: #343e4d; |
|
|
} |
|
|
.swiper-slide img, .swiper-slide video { |
|
|
max-width: 100%; |
|
|
max-height: 350px; |
|
|
object-fit: contain; |
|
|
} |
|
|
.swiper-button-next, .swiper-button-prev { |
|
|
color: var(--primary-color) !important; |
|
|
background-color: rgba(255,255,255,0.9); |
|
|
border-radius: 50%; |
|
|
width: 45px; |
|
|
height: 45px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
transition: background-color 0.3s; |
|
|
font-size: 1.8rem; |
|
|
} |
|
|
.swiper-button-next:hover, .swiper-button-prev:hover { |
|
|
background-color: #fff; |
|
|
} |
|
|
body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev { |
|
|
background-color: rgba(45, 55, 72, 0.9); |
|
|
} |
|
|
body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover { |
|
|
background-color: #2d3748; |
|
|
} |
|
|
.swiper-pagination-bullet { |
|
|
background-color: var(--primary-color) !important; |
|
|
} |
|
|
|
|
|
.product-detail-modal-inner { |
|
|
background: transparent; |
|
|
padding: 0; |
|
|
margin: 0; |
|
|
box-shadow: none; |
|
|
position: static; |
|
|
} |
|
|
body.dark-mode .product-detail-modal-inner { |
|
|
background: transparent; |
|
|
box-shadow: none; |
|
|
} |
|
|
.product-detail-modal-inner h2 { |
|
|
font-size: 2.2rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 25px; |
|
|
text-align: center; |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode .product-detail-modal-inner h2 { |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
.product-detail-modal-inner p { |
|
|
margin-bottom: 10px; |
|
|
font-size: 1rem; |
|
|
} |
|
|
.product-detail-modal-inner p strong { |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode .product-detail-modal-inner p strong { |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
.product-detail-modal-inner .price { |
|
|
font-size: 1.8rem; |
|
|
color: var(--danger-color); |
|
|
font-weight: 700; |
|
|
margin-bottom: 20px; |
|
|
text-align: center; |
|
|
} |
|
|
.product-detail-modal-inner .description { |
|
|
margin-bottom: 20px; |
|
|
white-space: pre-wrap; |
|
|
} |
|
|
.product-detail-modal-actions { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 15px; |
|
|
margin-top: 30px; |
|
|
} |
|
|
.product-detail-modal-actions .product-button { |
|
|
width: auto; |
|
|
padding: 12px 25px; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.container { |
|
|
padding: 15px; |
|
|
} |
|
|
.header h1 { |
|
|
font-size: 1.6rem; |
|
|
} |
|
|
.category-filter { |
|
|
font-size: 0.85rem; |
|
|
padding: 8px 15px; |
|
|
} |
|
|
.products-grid { |
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
|
|
gap: 18px; |
|
|
} |
|
|
.product h2 { |
|
|
font-size: 1.05rem; |
|
|
} |
|
|
.product-price { |
|
|
font-size: 1.15rem; |
|
|
} |
|
|
.product-description { |
|
|
font-size: 0.8rem; |
|
|
} |
|
|
.product-button { |
|
|
font-size: 0.9rem; |
|
|
padding: 10px; |
|
|
} |
|
|
#cart-button { |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
font-size: 1.2rem; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
} |
|
|
.modal-content { |
|
|
margin: 20px auto; |
|
|
padding: 25px; |
|
|
max-height: 95vh; |
|
|
} |
|
|
.close { |
|
|
font-size: 1.6rem; |
|
|
top: 15px; |
|
|
right: 18px; |
|
|
} |
|
|
.modal h2 { |
|
|
font-size: 1.6rem; |
|
|
} |
|
|
.cart-item img { |
|
|
width: 55px; |
|
|
height: 55px; |
|
|
} |
|
|
.product-detail-modal-inner h2 { |
|
|
font-size: 2rem; |
|
|
} |
|
|
.product-detail-modal-inner .price { |
|
|
font-size: 1.6rem; |
|
|
} |
|
|
.product-detail-modal-actions { |
|
|
flex-direction: column; |
|
|
gap: 10px; |
|
|
} |
|
|
} |
|
|
''' |
|
|
|
|
|
@app.route('/') |
|
|
def catalog(): |
|
|
data = load_data() |
|
|
products = data.get('products', []) |
|
|
categories = data.get('categories', []) |
|
|
|
|
|
products.sort(key=lambda p: p['name'].lower()) |
|
|
|
|
|
return render_template_string(''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>ZZIRIX - сотовые аксессуары оптом</title> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css"> |
|
|
<style>''' + BASE_STYLE + '''</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<div class="header-info"> |
|
|
<img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo"> |
|
|
<h1>Каталог ZZIRIX</h1> |
|
|
</div> |
|
|
<button class="theme-toggle" onclick="toggleTheme()"> |
|
|
<i class="fas fa-moon"></i> |
|
|
</button> |
|
|
</div> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<div class="filters-container"> |
|
|
<button class="category-filter active" data-category="all">Все категории</button> |
|
|
{% for category in categories %} |
|
|
<button class="category-filter" data-category="{{ category | lower }}">{{ category }}</button> |
|
|
{% endfor %} |
|
|
</div> |
|
|
<div class="search-container"> |
|
|
<input type="text" id="search-input" placeholder="Поиск товаров..."> |
|
|
</div> |
|
|
<div class="products-grid" id="products-grid"> |
|
|
{% for product in products %} |
|
|
<div class="product" |
|
|
data-name="{{ product['name']|lower }}" |
|
|
data-description="{{ product['description']|lower }}" |
|
|
data-category="{{ product.get('category', 'Без категории')|lower }}"> |
|
|
<a href="javascript:void(0)" onclick="openProductDetailModal('{{ product.id }}')" class="product-image"> |
|
|
{% if product.get('photos') and product['photos']|length > 0 %} |
|
|
{% set filename = product['photos'][0] %} |
|
|
{% if filename.endswith(('.mp4', '.mov', '.webm')) %} |
|
|
<video preload="metadata" muted loop autoplay playsinline><source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}" type="video/mp4"></video> |
|
|
{% else %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}" |
|
|
alt="{{ product['name'] }}" |
|
|
loading="lazy"> |
|
|
{% endif %} |
|
|
{% else %} |
|
|
<img src="https://via.placeholder.com/200x200?text=No+Image" |
|
|
alt="Нет изображения" |
|
|
loading="lazy"> |
|
|
{% endif %} |
|
|
</a> |
|
|
<h2>{{ product['name'] }}</h2> |
|
|
<div class="product-price">{{ "%.2f"|format(product['price']) }} с</div> |
|
|
<p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p> |
|
|
<div class="product-actions"> |
|
|
<button class="product-button" onclick="openProductDetailModal('{{ product.id }}')">Подробнее</button> |
|
|
<button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button> |
|
|
</div> |
|
|
</div> |
|
|
{% else %} |
|
|
<p style="grid-column: 1 / -1; text-align: center; padding: 40px 0;">Товаров пока нет. Загляните позже!</p> |
|
|
{% endfor %} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="quantityModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<span class="close" onclick="closeModal('quantityModal')">×</span> |
|
|
<h2>Укажите количество, цвет и модель</h2> |
|
|
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1"> |
|
|
<select id="colorSelect" class="color-select"></select> |
|
|
<select id="modelSelect" class="model-select"></select> |
|
|
<div class="modal-buttons"> |
|
|
<button class="product-button order-button" onclick="confirmAddToCart()">Добавить</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="cartModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<span class="close" onclick="closeModal('cartModal')">×</span> |
|
|
<h2>Корзина</h2> |
|
|
<div id="cartContent"></div> |
|
|
<div style="margin-top: 20px; text-align: right;"> |
|
|
<strong style="font-size: 1.2rem;">Итого: <span id="cartTotal">0</span> с</strong> |
|
|
<div class="modal-buttons"> |
|
|
<button class="product-button clear-cart" onclick="clearCart()">Очистить</button> |
|
|
<button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="productDetailModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<span class="close" onclick="closeModal('productDetailModal')">×</span> |
|
|
<div class="product-detail-modal-inner"> |
|
|
<h2 id="detailProductName"></h2> |
|
|
<div class="swiper-container" style="max-width: 500px; margin: 0 auto 30px;"> |
|
|
<div class="swiper-wrapper" id="detailProductMedia"> |
|
|
</div> |
|
|
<div class="swiper-pagination"></div> |
|
|
<div class="swiper-button-next"></div> |
|
|
<div class="swiper-button-prev"></div> |
|
|
</div> |
|
|
<p><strong>Категория:</strong> <span id="detailProductCategory" style="color: var(--primary-color);"></span></p> |
|
|
<p class="price" id="detailProductPrice"></p> |
|
|
<p class="description"><strong>Описание:</strong> <span id="detailProductDescription"></span></p> |
|
|
<p><strong>Доступные цвета:</strong> <span id="detailProductColors" style="color: var(--secondary-text-color-light);"></span></p> |
|
|
<p><strong>Доступные модели:</strong> <span id="detailProductModels" style="color: var(--secondary-text-color-light);"></span></p> |
|
|
<div class="product-detail-modal-actions"> |
|
|
<button class="product-button add-to-cart" id="detailAddToCartButton">В корзину</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button id="cart-button" onclick="openCartModal()"> |
|
|
<i class="fas fa-shopping-cart"></i> |
|
|
<span id="cart-count" class="cart-count">0</span> |
|
|
</button> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script> |
|
|
<script> |
|
|
const products = {{ products|tojson }}; |
|
|
let selectedProductId = null; |
|
|
let detailSwiperInstance = null; |
|
|
|
|
|
function toggleTheme() { |
|
|
document.body.classList.toggle('dark-mode'); |
|
|
const icon = document.querySelector('.theme-toggle i'); |
|
|
icon.classList.toggle('fa-moon'); |
|
|
icon.classList.toggle('fa-sun'); |
|
|
localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light'); |
|
|
} |
|
|
|
|
|
if (localStorage.getItem('theme') === 'dark') { |
|
|
document.body.classList.add('dark-mode'); |
|
|
const icon = document.querySelector('.theme-toggle i'); |
|
|
if (icon) icon.classList.replace('fa-moon', 'fa-sun'); |
|
|
} else { |
|
|
const icon = document.querySelector('.theme-toggle i'); |
|
|
if (icon) icon.classList.replace('fa-sun', 'fa-moon'); |
|
|
} |
|
|
|
|
|
function closeModal(modalId) { |
|
|
document.getElementById(modalId).style.display = "none"; |
|
|
if (modalId === 'productDetailModal' && detailSwiperInstance) { |
|
|
detailSwiperInstance.destroy(true, true); |
|
|
detailSwiperInstance = null; |
|
|
} |
|
|
} |
|
|
|
|
|
function openQuantityModal(productId) { |
|
|
selectedProductId = productId; |
|
|
const product = products.find(p => p.id === productId); |
|
|
if (!product) { |
|
|
alert('Ошибка: Товар не найден.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const colorSelect = document.getElementById('colorSelect'); |
|
|
colorSelect.innerHTML = ''; |
|
|
if (product.colors && product.colors.length > 0) { |
|
|
product.colors.forEach(color => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = color; |
|
|
option.text = color; |
|
|
colorSelect.appendChild(option); |
|
|
}); |
|
|
} else { |
|
|
const option = document.createElement('option'); |
|
|
option.value = 'Не указан'; |
|
|
option.text = 'Цвет не указан'; |
|
|
colorSelect.appendChild(option); |
|
|
} |
|
|
|
|
|
const modelSelect = document.getElementById('modelSelect'); |
|
|
modelSelect.innerHTML = ''; |
|
|
if (product.models && product.models.length > 0) { |
|
|
product.models.forEach(model => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = model; |
|
|
option.text = model; |
|
|
modelSelect.appendChild(option); |
|
|
}); |
|
|
} else { |
|
|
const option = document.createElement('option'); |
|
|
option.value = 'Не указана'; |
|
|
option.text = 'Модель не указана'; |
|
|
modelSelect.appendChild(option); |
|
|
} |
|
|
|
|
|
document.getElementById('quantityModal').style.display = 'block'; |
|
|
document.getElementById('quantityInput').value = 1; |
|
|
} |
|
|
|
|
|
function confirmAddToCart() { |
|
|
if (selectedProductId === null) return; |
|
|
const quantityInput = document.getElementById('quantityInput'); |
|
|
const quantity = parseInt(quantityInput.value) || 1; |
|
|
const color = document.getElementById('colorSelect').value; |
|
|
const model = document.getElementById('modelSelect').value; |
|
|
|
|
|
if (quantity <= 0) { |
|
|
alert("Укажите количество больше 0."); |
|
|
quantityInput.focus(); |
|
|
return; |
|
|
} |
|
|
|
|
|
let cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
const product = products.find(p => p.id === selectedProductId); |
|
|
|
|
|
const cartItemId = `${product.id}-${color}-${model}`; |
|
|
const existingItem = cart.find(item => item.cartId === cartItemId); |
|
|
|
|
|
if (existingItem) { |
|
|
existingItem.quantity += quantity; |
|
|
} else { |
|
|
cart.push({ |
|
|
cartId: cartItemId, |
|
|
productId: product.id, |
|
|
name: product.name, |
|
|
price: product.price, |
|
|
photo: product.photos && product.photos.length > 0 ? product.photos[0] : '', |
|
|
quantity: quantity, |
|
|
color: color, |
|
|
model: model |
|
|
}); |
|
|
} |
|
|
|
|
|
localStorage.setItem('cart', JSON.stringify(cart)); |
|
|
closeModal('quantityModal'); |
|
|
updateCartButton(); |
|
|
alert(`"${product.name}" добавлен в корзину.`); |
|
|
} |
|
|
|
|
|
function updateCartButton() { |
|
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0); |
|
|
document.getElementById('cart-count').textContent = cartCount; |
|
|
document.getElementById('cart-button').style.display = cartCount > 0 ? 'flex' : 'none'; |
|
|
} |
|
|
|
|
|
function openCartModal() { |
|
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
const cartContent = document.getElementById('cartContent'); |
|
|
let total = 0; |
|
|
|
|
|
cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center; color: var(--secondary-text-color-light);">Корзина пуста</p>' : cart.map((item, index) => { |
|
|
const itemTotal = item.price * item.quantity; |
|
|
total += itemTotal; |
|
|
const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60?text=No+Image'; |
|
|
return ` |
|
|
<div class="cart-item"> |
|
|
<img src="${photoSrc}" alt="${item.name}"> |
|
|
<div class="cart-item-details"> |
|
|
<strong>${item.name}</strong> |
|
|
<p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p> |
|
|
</div> |
|
|
<span class="cart-item-total">${itemTotal.toFixed(2)} с</span> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
document.getElementById('cartTotal').textContent = total.toFixed(2); |
|
|
document.getElementById('cartModal').style.display = 'block'; |
|
|
} |
|
|
|
|
|
function orderViaWhatsApp() { |
|
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
if (cart.length === 0) { |
|
|
alert("Корзина пуста!"); |
|
|
return; |
|
|
} |
|
|
let total = 0; |
|
|
let orderText = "Здравствуйте, меня интересует заказ:%0A%0A"; |
|
|
cart.forEach((item, index) => { |
|
|
const itemTotal = item.price * item.quantity; |
|
|
total += itemTotal; |
|
|
orderText += `${index + 1}. ${item.name}%0AКоличество: ${item.quantity}%0AЦена: ${item.price.toFixed(2)} с%0AЦвет: ${item.color}%0AМодель: ${item.model}%0A--%0A`; |
|
|
}); |
|
|
orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`; |
|
|
window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank'); |
|
|
clearCart(); |
|
|
} |
|
|
|
|
|
function clearCart() { |
|
|
localStorage.removeItem('cart'); |
|
|
openCartModal(); |
|
|
updateCartButton(); |
|
|
} |
|
|
|
|
|
window.onclick = function(event) { |
|
|
if (event.target.classList.contains('modal')) { |
|
|
event.target.style.display = "none"; |
|
|
if (detailSwiperInstance) { |
|
|
detailSwiperInstance.destroy(true, true); |
|
|
detailSwiperInstance = null; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
document.getElementById('search-input').addEventListener('input', filterProducts); |
|
|
document.querySelectorAll('.filters-container .category-filter').forEach(filter => { |
|
|
filter.addEventListener('click', function() { |
|
|
document.querySelectorAll('.filters-container .category-filter').forEach(f => f.classList.remove('active')); |
|
|
this.classList.add('active'); |
|
|
filterProducts(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
function filterProducts() { |
|
|
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim(); |
|
|
const activeCategory = document.querySelector('.category-filter.active').dataset.category.toLowerCase(); |
|
|
document.querySelectorAll('.product').forEach(product => { |
|
|
const name = product.getAttribute('data-name'); |
|
|
const description = product.getAttribute('data-description'); |
|
|
const category = product.getAttribute('data-category'); |
|
|
|
|
|
const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm)); |
|
|
const matchesCategory = activeCategory === 'all' || category === activeCategory; |
|
|
|
|
|
product.style.display = matchesSearch && matchesCategory ? 'flex' : 'none'; |
|
|
}); |
|
|
} |
|
|
|
|
|
function openProductDetailModal(productId) { |
|
|
selectedProductId = productId; |
|
|
const product = products.find(p => p.id === productId); |
|
|
if (!product) { |
|
|
alert('Ошибка: Товар не найден.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
document.getElementById('detailProductName').textContent = product.name; |
|
|
document.getElementById('detailProductCategory').textContent = product.category || 'Без категории'; |
|
|
document.getElementById('detailProductPrice').textContent = `${product.price.toFixed(2)} с`; |
|
|
document.getElementById('detailProductDescription').textContent = product.description; |
|
|
document.getElementById('detailProductColors').textContent = product.colors && product.colors.length > 0 ? product.colors.join(', ') : 'Нет цветов'; |
|
|
document.getElementById('detailProductModels').textContent = product.models && product.models.length > 0 ? product.models.join(', ') : 'Нет моделей'; |
|
|
document.getElementById('detailAddToCartButton').onclick = () => openQuantityModal(product.id); |
|
|
|
|
|
const mediaContainer = document.getElementById('detailProductMedia'); |
|
|
mediaContainer.innerHTML = ''; |
|
|
if (product.photos && product.photos.length > 0) { |
|
|
product.photos.forEach(filename => { |
|
|
const slide = document.createElement('div'); |
|
|
slide.className = 'swiper-slide swiper-zoom-container'; |
|
|
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;"; |
|
|
|
|
|
const mediaUrl = `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${filename}`; |
|
|
if (filename.endsWith(('.mp4', '.mov', '.webm'))) { |
|
|
slide.innerHTML = `<video controls preload="metadata" style="max-width: 100%; max-height: 350px; object-fit: contain;"><source src="${mediaUrl}" type="video/mp4"></video>`; |
|
|
} else { |
|
|
slide.innerHTML = `<img src="${mediaUrl}" alt="${product.name}" style="max-width: 100%; max-height: 350px; object-fit: contain;">`; |
|
|
} |
|
|
mediaContainer.appendChild(slide); |
|
|
}); |
|
|
} else { |
|
|
const slide = document.createElement('div'); |
|
|
slide.className = 'swiper-slide swiper-zoom-container'; |
|
|
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;"; |
|
|
slide.innerHTML = `<img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">`; |
|
|
mediaContainer.appendChild(slide); |
|
|
} |
|
|
|
|
|
document.getElementById('productDetailModal').style.display = 'block'; |
|
|
|
|
|
if (detailSwiperInstance) { |
|
|
detailSwiperInstance.destroy(true, true); |
|
|
} |
|
|
detailSwiperInstance = new Swiper('#productDetailModal .swiper-container', { |
|
|
slidesPerView: 1, |
|
|
spaceBetween: 20, |
|
|
loop: true, |
|
|
grabCursor: true, |
|
|
pagination: { el: '#productDetailModal .swiper-pagination', clickable: true }, |
|
|
navigation: { nextEl: '#productDetailModal .swiper-button-next', prevEl: '#productDetailModal .swiper-button-prev' }, |
|
|
zoom: { maxRatio: 3 }, |
|
|
on: { |
|
|
init: function () { |
|
|
this.slides.forEach(slide => { |
|
|
const img = slide.querySelector('img'); |
|
|
if (img && !img.complete) { |
|
|
img.onload = () => detailSwiperInstance.update(); |
|
|
} |
|
|
const video = slide.querySelector('video'); |
|
|
if (video) { |
|
|
video.onloadedmetadata = () => detailSwiperInstance.update(); |
|
|
} |
|
|
}); |
|
|
}, |
|
|
resize: function() { |
|
|
this.update(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
detailSwiperInstance.update(); |
|
|
} |
|
|
|
|
|
updateCartButton(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''', |
|
|
products=products, |
|
|
categories=categories, |
|
|
repo_id=REPO_ID, |
|
|
LOGO_URL=LOGO_URL |
|
|
) |
|
|
|
|
|
@app.route('/category/<category_name>') |
|
|
def catalog_by_category(category_name): |
|
|
data = load_data() |
|
|
products_all = data.get('products', []) |
|
|
categories = data.get('categories', []) |
|
|
|
|
|
products = [p for p in products_all if p.get('category', '').lower() == category_name.lower()] |
|
|
products.sort(key=lambda p: p['name'].lower()) |
|
|
|
|
|
return render_template_string(''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>{{ category_name }} - ZZIRIX</title> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css"> |
|
|
<style>''' + BASE_STYLE + '''</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<div class="header-info"> |
|
|
<img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo"> |
|
|
<h1>Каталог ZZIRIX</h1> |
|
|
</div> |
|
|
<button class="theme-toggle" onclick="toggleTheme()"> |
|
|
<i class="fas fa-moon"></i> |
|
|
</button> |
|
|
</div> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<div class="filters-container"> |
|
|
<button class="category-filter" data-category="all" onclick="window.location.href='{{ url_for('catalog') }}'">Все категории</button> |
|
|
{% for cat in categories %} |
|
|
<button class="category-filter {% if cat | lower == category_name | lower %}active{% endif %}" data-category="{{ cat | lower }}" onclick="window.location.href='{{ url_for('catalog_by_category', category_name=cat | lower) }}'">{{ cat }}</button> |
|
|
{% endfor %} |
|
|
</div> |
|
|
<div class="search-container"> |
|
|
<input type="text" id="search-input" placeholder="Поиск товаров..."> |
|
|
</div> |
|
|
<div class="products-grid" id="products-grid"> |
|
|
{% for product in products %} |
|
|
<div class="product" |
|
|
data-name="{{ product['name']|lower }}" |
|
|
data-description="{{ product['description']|lower }}" |
|
|
data-category="{{ product.get('category', 'Без категории')|lower }}"> |
|
|
<a href="javascript:void(0)" onclick="openProductDetailModal('{{ product.id }}')" class="product-image"> |
|
|
{% if product.get('photos') and product['photos']|length > 0 %} |
|
|
{% set filename = product['photos'][0] %} |
|
|
{% if filename.endswith(('.mp4', '.mov', '.webm')) %} |
|
|
<video preload="metadata" muted loop autoplay playsinline><source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}" type="video/mp4"></video> |
|
|
{% else %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}" |
|
|
alt="{{ product['name'] }}" |
|
|
loading="lazy"> |
|
|
{% endif %} |
|
|
{% else %} |
|
|
<img src="https://via.placeholder.com/200x200?text=No+Image" |
|
|
alt="Нет изображения" |
|
|
loading="lazy"> |
|
|
{% endif %} |
|
|
</a> |
|
|
<h2>{{ product['name'] }}</h2> |
|
|
<div class="product-price">{{ "%.2f"|format(product['price']) }} с</div> |
|
|
<p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p> |
|
|
<div class="product-actions"> |
|
|
<button class="product-button" onclick="openProductDetailModal('{{ product.id }}')">Подробнее</button> |
|
|
<button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button> |
|
|
</div> |
|
|
</div> |
|
|
{% else %} |
|
|
<p style="grid-column: 1 / -1; text-align: center; padding: 40px 0;">В этой категории пока нет товаров.</p> |
|
|
{% endfor %} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="quantityModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<span class="close" onclick="closeModal('quantityModal')">×</span> |
|
|
<h2>Укажите количество, цвет и модель</h2> |
|
|
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1"> |
|
|
<select id="colorSelect" class="color-select"></select> |
|
|
<select id="modelSelect" class="model-select"></select> |
|
|
<div class="modal-buttons"> |
|
|
<button class="product-button order-button" onclick="confirmAddToCart()">Добавить</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="cartModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<span class="close" onclick="closeModal('cartModal')">×</span> |
|
|
<h2>Корзина</h2> |
|
|
<div id="cartContent"></div> |
|
|
<div style="margin-top: 20px; text-align: right;"> |
|
|
<strong style="font-size: 1.2rem;">Итого: <span id="cartTotal">0</span> с</strong> |
|
|
<div class="modal-buttons"> |
|
|
<button class="product-button clear-cart" onclick="clearCart()">Очистить</button> |
|
|
<button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="productDetailModal" class="modal"> |
|
|
<div class="modal-content"> |
|
|
<span class="close" onclick="closeModal('productDetailModal')">×</span> |
|
|
<div class="product-detail-modal-inner"> |
|
|
<h2 id="detailProductName"></h2> |
|
|
<div class="swiper-container" style="max-width: 500px; margin: 0 auto 30px;"> |
|
|
<div class="swiper-wrapper" id="detailProductMedia"> |
|
|
</div> |
|
|
<div class="swiper-pagination"></div> |
|
|
<div class="swiper-button-next"></div> |
|
|
<div class="swiper-button-prev"></div> |
|
|
</div> |
|
|
<p><strong>Категория:</strong> <span id="detailProductCategory" style="color: var(--primary-color);"></span></p> |
|
|
<p class="price" id="detailProductPrice"></p> |
|
|
<p class="description"><strong>Описание:</strong> <span id="detailProductDescription"></span></p> |
|
|
<p><strong>Доступные цвета:</strong> <span id="detailProductColors" style="color: var(--secondary-text-color-light);"></span></p> |
|
|
<p><strong>Доступные модели:</strong> <span id="detailProductModels" style="color: var(--secondary-text-color-light);"></span></p> |
|
|
<div class="product-detail-modal-actions"> |
|
|
<button class="product-button add-to-cart" id="detailAddToCartButton">В корзину</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button id="cart-button" onclick="openCartModal()"> |
|
|
<i class="fas fa-shopping-cart"></i> |
|
|
<span id="cart-count" class="cart-count">0</span> |
|
|
</button> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script> |
|
|
<script> |
|
|
const products = {{ products_all|tojson }}; |
|
|
let selectedProductId = null; |
|
|
let detailSwiperInstance = null; |
|
|
|
|
|
function toggleTheme() { |
|
|
document.body.classList.toggle('dark-mode'); |
|
|
const icon = document.querySelector('.theme-toggle i'); |
|
|
icon.classList.toggle('fa-moon'); |
|
|
icon.classList.toggle('fa-sun'); |
|
|
localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light'); |
|
|
} |
|
|
|
|
|
if (localStorage.getItem('theme') === 'dark') { |
|
|
document.body.classList.add('dark-mode'); |
|
|
const icon = document.querySelector('.theme-toggle i'); |
|
|
if (icon) icon.classList.replace('fa-moon', 'fa-sun'); |
|
|
} else { |
|
|
const icon = document.querySelector('.theme-toggle i'); |
|
|
if (icon) icon.classList.replace('fa-sun', 'fa-moon'); |
|
|
} |
|
|
|
|
|
function closeModal(modalId) { |
|
|
document.getElementById(modalId).style.display = "none"; |
|
|
if (modalId === 'productDetailModal' && detailSwiperInstance) { |
|
|
detailSwiperInstance.destroy(true, true); |
|
|
detailSwiperInstance = null; |
|
|
} |
|
|
} |
|
|
|
|
|
function openQuantityModal(productId) { |
|
|
selectedProductId = productId; |
|
|
const product = products.find(p => p.id === productId); |
|
|
if (!product) { |
|
|
alert('Ошибка: Товар не найден.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const colorSelect = document.getElementById('colorSelect'); |
|
|
colorSelect.innerHTML = ''; |
|
|
if (product.colors && product.colors.length > 0) { |
|
|
product.colors.forEach(color => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = color; |
|
|
option.text = color; |
|
|
colorSelect.appendChild(option); |
|
|
}); |
|
|
} else { |
|
|
const option = document.createElement('option'); |
|
|
option.value = 'Не указан'; |
|
|
option.text = 'Цвет не указан'; |
|
|
colorSelect.appendChild(option); |
|
|
} |
|
|
|
|
|
const modelSelect = document.getElementById('modelSelect'); |
|
|
modelSelect.innerHTML = ''; |
|
|
if (product.models && product.models.length > 0) { |
|
|
product.models.forEach(model => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = model; |
|
|
option.text = model; |
|
|
modelSelect.appendChild(option); |
|
|
}); |
|
|
} else { |
|
|
const option = document.createElement('option'); |
|
|
option.value = 'Не указана'; |
|
|
option.text = 'Модель не указана'; |
|
|
modelSelect.appendChild(option); |
|
|
} |
|
|
|
|
|
document.getElementById('quantityModal').style.display = 'block'; |
|
|
document.getElementById('quantityInput').value = 1; |
|
|
} |
|
|
|
|
|
function confirmAddToCart() { |
|
|
if (selectedProductId === null) return; |
|
|
const quantityInput = document.getElementById('quantityInput'); |
|
|
const quantity = parseInt(quantityInput.value) || 1; |
|
|
const color = document.getElementById('colorSelect').value; |
|
|
const model = document.getElementById('modelSelect').value; |
|
|
|
|
|
if (quantity <= 0) { |
|
|
alert("Укажите количество больше 0."); |
|
|
quantityInput.focus(); |
|
|
return; |
|
|
} |
|
|
|
|
|
let cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
const product = products.find(p => p.id === selectedProductId); |
|
|
|
|
|
const cartItemId = `${product.id}-${color}-${model}`; |
|
|
const existingItem = cart.find(item => item.cartId === cartItemId); |
|
|
|
|
|
if (existingItem) { |
|
|
existingItem.quantity += quantity; |
|
|
} else { |
|
|
cart.push({ |
|
|
cartId: cartItemId, |
|
|
productId: product.id, |
|
|
name: product.name, |
|
|
price: product.price, |
|
|
photo: product.photos && product.photos.length > 0 ? product.photos[0] : '', |
|
|
quantity: quantity, |
|
|
color: color, |
|
|
model: model |
|
|
}); |
|
|
} |
|
|
|
|
|
localStorage.setItem('cart', JSON.stringify(cart)); |
|
|
closeModal('quantityModal'); |
|
|
updateCartButton(); |
|
|
alert(`"${product.name}" добавлен в корзину.`); |
|
|
} |
|
|
|
|
|
function updateCartButton() { |
|
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0); |
|
|
document.getElementById('cart-count').textContent = cartCount; |
|
|
document.getElementById('cart-button').style.display = cartCount > 0 ? 'flex' : 'none'; |
|
|
} |
|
|
|
|
|
function openCartModal() { |
|
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
const cartContent = document.getElementById('cartContent'); |
|
|
let total = 0; |
|
|
|
|
|
cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center; color: var(--secondary-text-color-light);">Корзина пуста</p>' : cart.map((item, index) => { |
|
|
const itemTotal = item.price * item.quantity; |
|
|
total += itemTotal; |
|
|
const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60?text=No+Image'; |
|
|
return ` |
|
|
<div class="cart-item"> |
|
|
<img src="${photoSrc}" alt="${item.name}"> |
|
|
<div class="cart-item-details"> |
|
|
<strong>${item.name}</strong> |
|
|
<p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p> |
|
|
</div> |
|
|
<span class="cart-item-total">${itemTotal.toFixed(2)} с</span> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
document.getElementById('cartTotal').textContent = total.toFixed(2); |
|
|
document.getElementById('cartModal').style.display = 'block'; |
|
|
} |
|
|
|
|
|
function orderViaWhatsApp() { |
|
|
const cart = JSON.parse(localStorage.getItem('cart') || '[]'); |
|
|
if (cart.length === 0) { |
|
|
alert("Корзина пуста!"); |
|
|
return; |
|
|
} |
|
|
let total = 0; |
|
|
let orderText = "Здравствуйте, меня интересует заказ:%0A%0A"; |
|
|
cart.forEach((item, index) => { |
|
|
const itemTotal = item.price * item.quantity; |
|
|
total += itemTotal; |
|
|
orderText += `${index + 1}. ${item.name}%0AКоличество: ${item.quantity}%0AЦена: ${item.price.toFixed(2)} с%0AЦвет: ${item.color}%0AМодель: ${item.model}%0A--%0A`; |
|
|
}); |
|
|
orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`; |
|
|
window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank'); |
|
|
clearCart(); |
|
|
} |
|
|
|
|
|
function clearCart() { |
|
|
localStorage.removeItem('cart'); |
|
|
openCartModal(); |
|
|
updateCartButton(); |
|
|
} |
|
|
|
|
|
window.onclick = function(event) { |
|
|
if (event.target.classList.contains('modal')) { |
|
|
event.target.style.display = "none"; |
|
|
if (detailSwiperInstance) { |
|
|
detailSwiperInstance.destroy(true, true); |
|
|
detailSwiperInstance = null; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
document.getElementById('search-input').addEventListener('input', filterProducts); |
|
|
|
|
|
function filterProducts() { |
|
|
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim(); |
|
|
const activeCategory = "{{ category_name | lower }}"; |
|
|
|
|
|
document.querySelectorAll('.product').forEach(product => { |
|
|
const name = product.getAttribute('data-name'); |
|
|
const description = product.getAttribute('data-description'); |
|
|
|
|
|
const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm)); |
|
|
|
|
|
product.style.display = matchesSearch ? 'flex' : 'none'; |
|
|
}); |
|
|
} |
|
|
|
|
|
function openProductDetailModal(productId) { |
|
|
selectedProductId = productId; |
|
|
const product = products.find(p => p.id === productId); |
|
|
if (!product) { |
|
|
alert('Ошибка: Товар не найден.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
document.getElementById('detailProductName').textContent = product.name; |
|
|
document.getElementById('detailProductCategory').textContent = product.category || 'Без категории'; |
|
|
document.getElementById('detailProductPrice').textContent = `${product.price.toFixed(2)} с`; |
|
|
document.getElementById('detailProductDescription').textContent = product.description; |
|
|
document.getElementById('detailProductColors').textContent = product.colors && product.colors.length > 0 ? product.colors.join(', ') : 'Нет цветов'; |
|
|
document.getElementById('detailProductModels').textContent = product.models && product.models.length > 0 ? product.models.join(', ') : 'Нет моделей'; |
|
|
document.getElementById('detailAddToCartButton').onclick = () => openQuantityModal(product.id); |
|
|
|
|
|
const mediaContainer = document.getElementById('detailProductMedia'); |
|
|
mediaContainer.innerHTML = ''; |
|
|
if (product.photos && product.photos.length > 0) { |
|
|
product.photos.forEach(filename => { |
|
|
const slide = document.createElement('div'); |
|
|
slide.className = 'swiper-slide swiper-zoom-container'; |
|
|
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;"; |
|
|
|
|
|
const mediaUrl = `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${filename}`; |
|
|
if (filename.endsWith(('.mp4', '.mov', '.webm'))) { |
|
|
slide.innerHTML = `<video controls preload="metadata" style="max-width: 100%; max-height: 350px; object-fit: contain;"><source src="${mediaUrl}" type="video/mp4"></video>`; |
|
|
} else { |
|
|
slide.innerHTML = `<img src="${mediaUrl}" alt="${product.name}" style="max-width: 100%; max-height: 350px; object-fit: contain;">`; |
|
|
} |
|
|
mediaContainer.appendChild(slide); |
|
|
}); |
|
|
} else { |
|
|
const slide = document.createElement('div'); |
|
|
slide.className = 'swiper-slide swiper-zoom-container'; |
|
|
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;"; |
|
|
slide.innerHTML = `<img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">`; |
|
|
mediaContainer.appendChild(slide); |
|
|
} |
|
|
|
|
|
document.getElementById('productDetailModal').style.display = 'block'; |
|
|
|
|
|
if (detailSwiperInstance) { |
|
|
detailSwiperInstance.destroy(true, true); |
|
|
} |
|
|
detailSwiperInstance = new Swiper('#productDetailModal .swiper-container', { |
|
|
slidesPerView: 1, |
|
|
spaceBetween: 20, |
|
|
loop: true, |
|
|
grabCursor: true, |
|
|
pagination: { el: '#productDetailModal .swiper-pagination', clickable: true }, |
|
|
navigation: { nextEl: '#productDetailModal .swiper-button-next', prevEl: '#productDetailModal .swiper-button-prev' }, |
|
|
zoom: { maxRatio: 3 }, |
|
|
on: { |
|
|
init: function () { |
|
|
this.slides.forEach(slide => { |
|
|
const img = slide.querySelector('img'); |
|
|
if (img && !img.complete) { |
|
|
img.onload = () => detailSwiperInstance.update(); |
|
|
} |
|
|
const video = slide.querySelector('video'); |
|
|
if (video) { |
|
|
video.onloadedmetadata = () => detailSwiperInstance.update(); |
|
|
} |
|
|
}); |
|
|
}, |
|
|
resize: function() { |
|
|
this.update(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
detailSwiperInstance.update(); |
|
|
} |
|
|
|
|
|
updateCartButton(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''', |
|
|
products=products, |
|
|
categories=categories, |
|
|
category_name=category_name, |
|
|
products_all=products_all, |
|
|
repo_id=REPO_ID, |
|
|
LOGO_URL=LOGO_URL |
|
|
) |
|
|
|
|
|
@app.route('/admin', methods=['GET', 'POST']) |
|
|
def admin(): |
|
|
data = load_data() |
|
|
products = data.get('products', []) |
|
|
categories = data.get('categories', []) |
|
|
|
|
|
if request.method == 'POST': |
|
|
action = request.form.get('action') |
|
|
api = HfApi() |
|
|
|
|
|
if action == 'add_category': |
|
|
category_name = request.form.get('category_name', '').strip() |
|
|
if category_name and category_name not in categories: |
|
|
categories.append(category_name) |
|
|
save_data(data) |
|
|
flash('Категория успешно добавлена.', 'success') |
|
|
return redirect(url_for('admin')) |
|
|
flash('Ошибка: Категория уже существует или не указано название.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
elif action == 'delete_category': |
|
|
category_name_to_delete = request.form.get('category_name') |
|
|
if category_name_to_delete in categories: |
|
|
categories.remove(category_name_to_delete) |
|
|
for product in products: |
|
|
if product.get('category') == category_name_to_delete: |
|
|
product['category'] = 'Без категории' |
|
|
save_data(data) |
|
|
flash('Категория удалена. Связанные товары теперь в категории "Без категории".', 'success') |
|
|
return redirect(url_for('admin')) |
|
|
flash('Ошибка: Категория не найдена.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
elif action == 'add': |
|
|
name = request.form.get('name', '').strip() |
|
|
price = request.form.get('price', '').strip() |
|
|
description = request.form.get('description', '').strip() |
|
|
category = request.form.get('category', 'Без категории').strip() |
|
|
photos_files = request.files.getlist('photos') |
|
|
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] |
|
|
models = [m.strip() for m in request.form.getlist('models') if m.strip()] |
|
|
|
|
|
if not name or not price or not description: |
|
|
flash('Ошибка: Заполните все обязательные поля (Название, Цена, Описание).', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
try: |
|
|
price = float(price.replace(',', '.')) |
|
|
except ValueError: |
|
|
flash('Ошибка: Неверный формат цены.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
photos_list = [] |
|
|
if photos_files and any(f.filename for f in photos_files): |
|
|
uploads_dir = 'uploads' |
|
|
os.makedirs(uploads_dir, exist_ok=True) |
|
|
for i, photo in enumerate(photos_files[:10]): |
|
|
if photo and allowed_file(photo.filename): |
|
|
base, extension = os.path.splitext(photo.filename) |
|
|
unique_filename = secure_filename(f"{name.replace(' ','_')}_{int(time.time())}_{i}{extension}") |
|
|
temp_path = os.path.join(uploads_dir, unique_filename) |
|
|
try: |
|
|
photo.save(temp_path) |
|
|
api.upload_file( |
|
|
path_or_fileobj=temp_path, |
|
|
path_in_repo=f"photos/{unique_filename}", |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Добавлено фото для товара {name}" |
|
|
) |
|
|
photos_list.append(unique_filename) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при загрузке фото {unique_filename}: {e}") |
|
|
flash(f'Ошибка при загрузке фото {photo.filename}: {e}', 'error') |
|
|
finally: |
|
|
if os.path.exists(temp_path): |
|
|
os.remove(temp_path) |
|
|
|
|
|
new_product = { |
|
|
'id': str(uuid.uuid4()), |
|
|
'name': name, |
|
|
'price': price, |
|
|
'description': description, |
|
|
'category': category if category in categories else 'Без категории', |
|
|
'photos': photos_list, |
|
|
'colors': colors, |
|
|
'models': models, |
|
|
'in_stock': True, |
|
|
'is_top': False |
|
|
} |
|
|
products.append(new_product) |
|
|
save_data(data) |
|
|
flash('Товар успешно добавлен.', 'success') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
elif action == 'edit': |
|
|
product_id = request.form.get('product_id') |
|
|
product_to_edit = next((p for p in products if p.get('id') == product_id), None) |
|
|
|
|
|
if not product_to_edit: |
|
|
flash('Ошибка: Товар не найден для редактирования.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
name = request.form.get('name', '').strip() |
|
|
price = request.form.get('price', '').strip() |
|
|
description = request.form.get('description', '').strip() |
|
|
category = request.form.get('category', 'Без категории').strip() |
|
|
photos_files = request.files.getlist('photos') |
|
|
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] |
|
|
models = [m.strip() for m in request.form.getlist('models') if m.strip()] |
|
|
|
|
|
if not name or not price or not description: |
|
|
flash('Ошибка: Заполните все обязательные поля (Название, Цена, Описание) при редактировании.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
try: |
|
|
price = float(price.replace(',', '.')) |
|
|
except ValueError: |
|
|
flash('Ошибка: Неверный формат цены при редактировании.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
product_to_edit['name'] = name |
|
|
product_to_edit['price'] = price |
|
|
product_to_edit['description'] = description |
|
|
product_to_edit['category'] = category if category in categories else 'Без категории' |
|
|
product_to_edit['colors'] = colors |
|
|
product_to_edit['models'] = models |
|
|
|
|
|
if photos_files and any(f.filename for f in photos_files): |
|
|
new_photos_list = [] |
|
|
uploads_dir = 'uploads' |
|
|
os.makedirs(uploads_dir, exist_ok=True) |
|
|
|
|
|
for old_filename in product_to_edit.get('photos', []): |
|
|
try: |
|
|
api.delete_file(path_in_repo=f"photos/{old_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) |
|
|
logging.info(f"Старое фото {old_filename} удалено из HF.") |
|
|
except Exception as e: |
|
|
logging.error(f"Не удалось удалить старое фото {old_filename} из HF: {e}") |
|
|
|
|
|
for i, photo in enumerate(photos_files[:10]): |
|
|
if photo and allowed_file(photo.filename): |
|
|
base, extension = os.path.splitext(photo.filename) |
|
|
unique_filename = secure_filename(f"{name.replace(' ','_')}_{int(time.time())}_{i}{extension}") |
|
|
temp_path = os.path.join(uploads_dir, unique_filename) |
|
|
try: |
|
|
photo.save(temp_path) |
|
|
api.upload_file( |
|
|
path_or_fileobj=temp_path, |
|
|
path_in_repo=f"photos/{unique_filename}", |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Обновлено фото для товара {name}" |
|
|
) |
|
|
new_photos_list.append(unique_filename) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при загрузке нового фото {unique_filename}: {e}") |
|
|
flash(f'Ошибка при загрузке нового фото {photo.filename}: {e}', 'error') |
|
|
finally: |
|
|
if os.path.exists(temp_path): |
|
|
os.remove(temp_path) |
|
|
|
|
|
product_to_edit['photos'] = new_photos_list |
|
|
|
|
|
save_data(data) |
|
|
flash('Товар успешно отредактирован.', 'success') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
elif action == 'delete': |
|
|
product_id = request.form.get('product_id') |
|
|
product_to_delete = next((p for p in products if p.get('id') == product_id), None) |
|
|
|
|
|
if product_to_delete: |
|
|
for filename in product_to_delete.get('photos', []): |
|
|
try: |
|
|
api.delete_file(path_in_repo=f"photos/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) |
|
|
logging.info(f"Фото {filename} удалено из HF.") |
|
|
except Exception as e: |
|
|
logging.error(f"Не удалось удалить фото {filename} из HF: {e}") |
|
|
|
|
|
data['products'] = [p for p in products if p.get('id') != product_id] |
|
|
save_data(data) |
|
|
flash('Товар удален.', 'success') |
|
|
return redirect(url_for('admin')) |
|
|
flash('Ошибка: Товар не найден для удаления.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
products.sort(key=lambda p: p['name'].lower()) |
|
|
|
|
|
return render_template_string(''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Админ-панель ZZIRIX</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> |
|
|
<style>''' + BASE_STYLE + ''' |
|
|
body { |
|
|
font-family: 'Roboto', sans-serif; |
|
|
background: var(--background-light); |
|
|
color: var(--text-color-light); |
|
|
padding: 20px; |
|
|
line-height: 1.6; |
|
|
transition: background 0.3s, color 0.3s; |
|
|
} |
|
|
.container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
.header { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 15px 0; |
|
|
border-bottom: 1px solid var(--border-color-light); |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.header-logo { |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
border-radius: 50%; |
|
|
object-fit: cover; |
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
|
|
margin-right: 15px; |
|
|
} |
|
|
.header-logo:hover { |
|
|
transform: scale(1.05); |
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
h1, h2 { |
|
|
font-weight: 700; |
|
|
margin-bottom: 25px; |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode h1, body.dark-mode h2 { |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
form { |
|
|
background: var(--card-background-light); |
|
|
padding: 30px; |
|
|
border-radius: var(--border-radius-large); |
|
|
box-shadow: var(--shadow-light); |
|
|
margin-bottom: 30px; |
|
|
transition: background 0.3s, box-shadow 0.3s; |
|
|
} |
|
|
body.dark-mode form { |
|
|
background: var(--card-background-dark); |
|
|
box-shadow: var(--shadow-dark); |
|
|
} |
|
|
label { |
|
|
font-weight: 600; |
|
|
margin-top: 18px; |
|
|
margin-bottom: 5px; |
|
|
display: block; |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode label { |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
input, textarea, select { |
|
|
width: 100%; |
|
|
padding: 12px; |
|
|
margin-bottom: 10px; |
|
|
border: 1px solid var(--border-color-light); |
|
|
border-radius: var(--border-radius-medium); |
|
|
font-size: 1rem; |
|
|
transition: all 0.3s ease; |
|
|
background-color: var(--card-background-light); |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode input, body.dark-mode textarea, body.dark-mode select { |
|
|
border-color: var(--border-color-dark); |
|
|
background-color: var(--card-background-dark); |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
input:focus, textarea:focus, select:focus { |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); |
|
|
outline: none; |
|
|
} |
|
|
button { |
|
|
padding: 12px 22px; |
|
|
border: none; |
|
|
border-radius: var(--border-radius-medium); |
|
|
background-color: var(--primary-color); |
|
|
color: white; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
margin-top: 15px; |
|
|
font-size: 1rem; |
|
|
} |
|
|
button:hover { |
|
|
background-color: var(--primary-dark-color); |
|
|
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
.delete-button { |
|
|
background-color: var(--danger-color); |
|
|
margin-left: 10px; |
|
|
} |
|
|
.delete-button:hover { |
|
|
background-color: var(--danger-dark-color); |
|
|
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4); |
|
|
} |
|
|
.product-list, .category-list { |
|
|
display: grid; |
|
|
gap: 20px; |
|
|
} |
|
|
.product-item, .category-item { |
|
|
background: var(--card-background-light); |
|
|
padding: 20px; |
|
|
border-radius: var(--border-radius-large); |
|
|
box-shadow: var(--shadow-light); |
|
|
transition: background 0.3s, box-shadow 0.3s; |
|
|
} |
|
|
body.dark-mode .product-item, body.dark-mode .category-item { |
|
|
background: var(--card-background-dark); |
|
|
box-shadow: var(--shadow-dark); |
|
|
} |
|
|
.product-item h3, .category-item h3 { |
|
|
font-size: 1.3rem; |
|
|
font-weight: 600; |
|
|
margin-bottom: 10px; |
|
|
color: var(--text-color-light); |
|
|
} |
|
|
body.dark-mode .product-item h3, body.dark-mode .category-item h3 { |
|
|
color: var(--text-color-dark); |
|
|
} |
|
|
.product-item p { |
|
|
font-size: 0.95rem; |
|
|
margin-bottom: 5px; |
|
|
color: var(--secondary-text-color-light); |
|
|
} |
|
|
body.dark-mode .product-item p { |
|
|
color: var(--secondary-text-color-dark); |
|
|
} |
|
|
.edit-form { |
|
|
margin-top: 20px; |
|
|
padding: 20px; |
|
|
background: var(--background-light); |
|
|
border-radius: var(--border-radius-medium); |
|
|
box-shadow: inset 0 2px 5px rgba(0,0,0,0.05); |
|
|
} |
|
|
body.dark-mode .edit-form { |
|
|
background: #3a4250; |
|
|
box-shadow: inset 0 2px 5px rgba(0,0,0,0.15); |
|
|
} |
|
|
.color-input-group, .model-input-group { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
margin-bottom: 10px; |
|
|
align-items: center; |
|
|
} |
|
|
.color-input-group input, .model-input-group input { |
|
|
flex-grow: 1; |
|
|
margin-bottom: 0; |
|
|
} |
|
|
.remove-input-btn { |
|
|
background-color: #ccc; |
|
|
color: #555; |
|
|
padding: 8px 12px; |
|
|
border-radius: var(--border-radius-small); |
|
|
font-size: 0.8rem; |
|
|
margin-top: 0; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
.remove-input-btn:hover { |
|
|
background-color: #bbb; |
|
|
box-shadow: none; |
|
|
transform: none; |
|
|
} |
|
|
.add-color-btn, .add-model-btn { |
|
|
background-color: var(--accent-color); |
|
|
width: auto; |
|
|
padding: 10px 18px; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
.add-color-btn:hover, .add-model-btn:hover { |
|
|
background-color: var(--accent-dark-color); |
|
|
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4); |
|
|
} |
|
|
.product-photos-preview { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 10px; |
|
|
margin-top: 10px; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
.product-photos-preview img, .product-photos-preview video { |
|
|
max-width: 90px; |
|
|
height: auto; |
|
|
border-radius: var(--border-radius-small); |
|
|
border: 1px solid var(--border-color-light); |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05); |
|
|
} |
|
|
body.dark-mode .product-photos-preview img, body.dark-mode .product-photos-preview video { |
|
|
border-color: var(--border-color-dark); |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15); |
|
|
} |
|
|
|
|
|
.admin-section-title { |
|
|
margin-top: 40px; |
|
|
margin-bottom: 20px; |
|
|
font-size: 1.8rem; |
|
|
color: var(--primary-color); |
|
|
} |
|
|
body.dark-mode .admin-section-title { |
|
|
color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.db-management-buttons button { |
|
|
margin-right: 10px; |
|
|
} |
|
|
|
|
|
.search-admin-container { |
|
|
margin: 20px 0; |
|
|
text-align: left; |
|
|
} |
|
|
#admin-search-input { |
|
|
width: 100%; |
|
|
max-width: 400px; |
|
|
margin-left: 0; |
|
|
border: 1px solid var(--border-color-light); |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); |
|
|
} |
|
|
body.dark-mode #admin-search-input { |
|
|
border-color: var(--border-color-dark); |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<div class="header-info"> |
|
|
<img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo"> |
|
|
<h1>Админ-панель</h1> |
|
|
</div> |
|
|
<a href="{{ url_for('catalog') }}" style="text-decoration: none;" class="product-button">Перейти в каталог</a> |
|
|
</div> |
|
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
|
|
|
<h2 class="admin-section-title">Добавление товара</h2> |
|
|
<form method="POST" enctype="multipart/form-data"> |
|
|
<input type="hidden" name="action" value="add"> |
|
|
<label>Название товара:</label> |
|
|
<input type="text" name="name" required> |
|
|
<label>Цена:</label> |
|
|
<input type="number" name="price" step="0.01" required> |
|
|
<label>Описание:</label> |
|
|
<textarea name="description" rows="4" required></textarea> |
|
|
<label>Категория:</label> |
|
|
<select name="category"> |
|
|
<option value="Без категории">Без категории</option> |
|
|
{% for category in categories %} |
|
|
<option value="{{ category }}">{{ category }}</option> |
|
|
{% endfor %} |
|
|
</select> |
|
|
<label>Фотографии и видео (до 10):</label> |
|
|
<input type="file" name="photos" accept="image/*,video/*" multiple> |
|
|
<label>Цвета:</label> |
|
|
<div id="color-inputs"> |
|
|
<div class="color-input-group"> |
|
|
<input type="text" name="colors" placeholder="Например: Красный"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
</div> |
|
|
</div> |
|
|
<button type="button" class="add-color-btn" onclick="addInput('color-inputs', 'colors', 'Цвет')">Добавить цвет</button> |
|
|
<label>Модели телефона:</label> |
|
|
<div id="model-inputs"> |
|
|
<div class="model-input-group"> |
|
|
<input type="text" name="models" placeholder="Например: iPhone 14"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
</div> |
|
|
</div> |
|
|
<button type="button" class="add-model-btn" onclick="addInput('model-inputs', 'models', 'Модель')">Добавить модель</button> |
|
|
<button type="submit">Добавить товар</button> |
|
|
</form> |
|
|
|
|
|
<h2 class="admin-section-title">Управление категориями</h2> |
|
|
<form method="POST"> |
|
|
<input type="hidden" name="action" value="add_category"> |
|
|
<label>Название категории:</label> |
|
|
<input type="text" name="category_name" required> |
|
|
<button type="submit">Добавить</button> |
|
|
</form> |
|
|
|
|
|
<h2>Список категорий</h2> |
|
|
<div class="category-list"> |
|
|
{% for category in categories %} |
|
|
<div class="category-item"> |
|
|
<h3>{{ category }}</h3> |
|
|
<form method="POST" style="display: inline;"> |
|
|
<input type="hidden" name="action" value="delete_category"> |
|
|
<input type="hidden" name="category_name" value="{{ category }}"> |
|
|
<button type="submit" class="delete-button">Удалить</button> |
|
|
</form> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
|
|
|
<h2 class="admin-section-title">Управление базой данных</h2> |
|
|
<div class="db-management-buttons"> |
|
|
<form method="POST" action="{{ url_for('backup') }}" style="display: inline;"> |
|
|
<button type="submit">Создать копию (вручную)</button> |
|
|
</form> |
|
|
<form method="GET" action="{{ url_for('download') }}" style="display: inline;"> |
|
|
<button type="submit">Скачать базу (JSON)</button> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
<h2 class="admin-section-title">Список товаров</h2> |
|
|
<div class="search-admin-container"> |
|
|
<input type="text" id="admin-search-input" placeholder="Поиск товаров в админ-панели..."> |
|
|
</div> |
|
|
<div class="product-list" id="admin-product-list"> |
|
|
{% for product in products %} |
|
|
<div class="product-item" |
|
|
data-name="{{ product['name']|lower }}" |
|
|
data-description="{{ product['description']|lower }}" |
|
|
data-category="{{ product.get('category', 'без категории')|lower }}"> |
|
|
<h3>{{ product['name'] }}</h3> |
|
|
<p><strong>ID:</strong> {{ product.id }}</p> |
|
|
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p> |
|
|
<p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} с</p> |
|
|
<p><strong>Описание:</strong> {{ product['description'] }}</p> |
|
|
<p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p> |
|
|
<p><strong>Модели:</strong> {{ product.get('models', ['Нет моделей'])|join(', ') }}</p> |
|
|
{% if product.get('photos') and product['photos']|length > 0 %} |
|
|
<div class="product-photos-preview"> |
|
|
{% for photo in product['photos'] %} |
|
|
{% if photo.endswith(('.mp4', '.mov', '.webm')) %} |
|
|
<video src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}" |
|
|
alt="{{ product['name'] }}" |
|
|
loading="lazy" |
|
|
controls |
|
|
style="max-width: 90px; height: auto; border-radius: var(--border-radius-small); border: 1px solid var(--border-color-light); box-shadow: 0 2px 8px rgba(0,0,0,0.05);"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}" |
|
|
alt="{{ product['name'] }}" |
|
|
loading="lazy"> |
|
|
{% endif %} |
|
|
{% endfor %} |
|
|
</div> |
|
|
{% endif %} |
|
|
<details> |
|
|
<summary style="font-weight: 500; cursor: pointer; color: var(--primary-color); margin-top: 15px;">Редактировать</summary> |
|
|
<form method="POST" enctype="multipart/form-data" class="edit-form"> |
|
|
<input type="hidden" name="action" value="edit"> |
|
|
<input type="hidden" name="product_id" value="{{ product.id }}"> |
|
|
<label>Название:</label> |
|
|
<input type="text" name="name" value="{{ product['name'] }}" required> |
|
|
<label>Цена:</label> |
|
|
<input type="number" name="price" step="0.01" value="{{ "%.2f"|format(product['price']) }}" required> |
|
|
<label>Описание:</label> |
|
|
<textarea name="description" rows="4" required>{{ product['description'] }}</textarea> |
|
|
<label>Категория:</label> |
|
|
<select name="category"> |
|
|
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option> |
|
|
{% for category in categories %} |
|
|
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option> |
|
|
{% endfor %} |
|
|
</select> |
|
|
<label>Фотографии и видео (заменят существующие, до 10):</label> |
|
|
<input type="file" name="photos" accept="image/*,video/*" multiple> |
|
|
<label>Цвета:</label> |
|
|
<div id="edit-color-inputs-{{ product.id }}"> |
|
|
{% for color in product.get('colors', []) %} |
|
|
<div class="color-input-group"> |
|
|
<input type="text" name="colors" value="{{ color }}"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
</div> |
|
|
{% endfor %} |
|
|
<div class="color-input-group"> |
|
|
<input type="text" name="colors" placeholder="Например: Новый цвет"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
</div> |
|
|
</div> |
|
|
<button type="button" class="add-color-btn" onclick="addInput('edit-color-inputs-{{ product.id }}', 'colors', 'Цвет')">Добавить цвет</button> |
|
|
<label>Модели телефона:</label> |
|
|
<div id="edit-model-inputs-{{ product.id }}"> |
|
|
{% for model in product.get('models', []) %} |
|
|
<div class="model-input-group"> |
|
|
<input type="text" name="models" value="{{ model }}"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
</div> |
|
|
{% endfor %} |
|
|
<div class="model-input-group"> |
|
|
<input type="text" name="models" placeholder="Например: Новая модель"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
</div> |
|
|
</div> |
|
|
<button type="button" class="add-model-btn" onclick="addInput('edit-model-inputs-{{ product.id }}', 'models', 'Модель')">Добавить модель</button> |
|
|
<button type="submit">Сохранить</button> |
|
|
</form> |
|
|
</details> |
|
|
<form method="POST" style="margin-top: 15px;"> |
|
|
<input type="hidden" name="action" value="delete"> |
|
|
<input type="hidden" name="product_id" value="{{ product.id }}"> |
|
|
<button type="submit" class="delete-button">Удалить товар</button> |
|
|
</form> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
function addInput(containerId, name, placeholderPrefix) { |
|
|
const container = document.getElementById(containerId); |
|
|
const newInput = document.createElement('div'); |
|
|
newInput.className = `${name.replace('s', '')}-input-group`; |
|
|
newInput.innerHTML = ` |
|
|
<input type="text" name="${name}" placeholder="Например: ${placeholderPrefix}"> |
|
|
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button> |
|
|
`; |
|
|
container.appendChild(newInput); |
|
|
} |
|
|
|
|
|
document.getElementById('admin-search-input').addEventListener('input', filterAdminProducts); |
|
|
|
|
|
function filterAdminProducts() { |
|
|
const searchTerm = document.getElementById('admin-search-input').value.toLowerCase().trim(); |
|
|
document.querySelectorAll('.product-item').forEach(productItem => { |
|
|
const name = productItem.getAttribute('data-name'); |
|
|
const description = productItem.getAttribute('data-description'); |
|
|
const category = productItem.getAttribute('data-category'); |
|
|
|
|
|
const matchesSearch = (name && name.includes(searchTerm)) || |
|
|
(description && description.includes(searchTerm)) || |
|
|
(category && category.includes(searchTerm)); |
|
|
|
|
|
productItem.style.display = matchesSearch ? 'block' : 'none'; |
|
|
}); |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''', |
|
|
products=products, |
|
|
categories=categories, |
|
|
repo_id=REPO_ID, |
|
|
LOGO_URL=LOGO_URL, |
|
|
random=random |
|
|
) |
|
|
|
|
|
@app.route('/backup', methods=['POST']) |
|
|
def backup(): |
|
|
try: |
|
|
data = load_data() |
|
|
save_data(data) |
|
|
flash('Резервная копия успешно создана и загружена.', 'success') |
|
|
return redirect(url_for('admin')) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при ручном создании резервной копии: {e}") |
|
|
flash(f'Ошибка при создании резервной копии: {e}', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
@app.route('/download', methods=['GET']) |
|
|
def download(): |
|
|
try: |
|
|
download_db_from_hf() |
|
|
if os.path.exists(DATA_FILE): |
|
|
return send_file(DATA_FILE, as_attachment=True, mimetype='application/json', download_name='data_zzirix_backup.json') |
|
|
flash('Файл базы данных не найден после попытки скачивания.', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка при скачивании базы данных: {e}") |
|
|
flash(f'Ошибка при скачивании базы данных: {e}', 'error') |
|
|
return redirect(url_for('admin')) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
uploads_dir = 'uploads' |
|
|
os.makedirs(uploads_dir, exist_ok=True) |
|
|
|
|
|
logging.info("Начальная проверка и обновление структуры данных...") |
|
|
try: |
|
|
current_data = load_data() |
|
|
save_data(current_data) |
|
|
logging.info("Структура данных проверена и при необходимости обновлена.") |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка во время начальной проверки/обновления структуры данных: {e}") |
|
|
|
|
|
backup_thread = threading.Thread(target=periodic_backup, daemon=True) |
|
|
backup_thread.start() |
|
|
logging.info("Запуск Flask приложения...") |
|
|
app.run(debug=True, host='0.0.0.0', port=7860) |