diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,4 @@ -from flask import Flask, render_template_string, request, redirect, url_for, flash, send_file +from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash import json import os import logging @@ -8,14 +8,12 @@ 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 +import uuid app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart") +app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart_and_flashes") 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 @@ -32,9 +30,6 @@ def initialize_data_structure(data): 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()) @@ -45,40 +40,49 @@ def initialize_data_structure(data): 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 'photos' not in product: + if 'media' in product: + product['photos'] = [item['filename'] for item in product['media'] if item['type'] == 'photo'] + del product['media'] + else: + product['photos'] = [] + if not isinstance(product['photos'], list): + product['photos'] = [] + if 'media' in product: del product['media'] + + product.pop('in_stock', None) + product.pop('is_top', None) - 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.") + try: download_db_from_hf() + except RepositoryNotFoundError: + logging.warning("Hugging Face repository not found or inaccessible. Proceeding with local data or empty structure.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump(initialize_data_structure({}), f) + except Exception as e: + logging.error(f"Error during initial HF download: {e}. Proceeding with local data or empty structure.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump(initialize_data_structure({}), f) 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.") + 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}. Создается локальная база данных.") + except (json.JSONDecodeError, FileNotFoundError): + logging.error(f"Ошибка при чтении {DATA_FILE}. Создается пустая структура.") return initialize_data_structure({}) except Exception as e: logging.error(f"Ошибка при загрузке данных: {e}") @@ -90,8 +94,8 @@ def save_data(data): 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) + logging.info("Данные успешно сохранены в JSON.") upload_db_to_hf() - logging.info("Данные сохранены и выгружены в HF") except Exception as e: logging.error(f"Ошибка при сохранении данных: {e}") if os.path.exists(temp_file): @@ -110,11 +114,11 @@ def upload_db_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Резервная копия {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("База данных выгружена в Hugging Face") + logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.") except Exception as e: - logging.error(f"Ошибка при выгрузке базы данных: {e}") + logging.error(f"Ошибка при загрузке резервной копии: {e}") def download_db_from_hf(): if not HF_TOKEN_READ: @@ -133,1560 +137,1005 @@ def download_db_from_hf(): 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) + logging.info("JSON база успешно скачана из Hugging Face.") + except RepositoryNotFoundError as e: + logging.error(f"Репозиторий не найден: {e}. Пропускаем скачивание.") + raise 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) + logging.error(f"Ошибка при скачивании JSON базы: {e}. Пропускаем скачивание.") + raise def periodic_backup(): while True: - time.sleep(3600) - logging.info("Запуск периодического резервного копирования.") try: - data = load_data() - save_data(data) + upload_db_to_hf() except Exception as e: - logging.error(f"Ошибка во время периодического резервного копирования: {e}") + logging.error(f"Ошибка при выполнении периодического резервного копирования: {e}") + time.sleep(3600) # Backup every 1 hour 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; - } -} -''' + filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'webp'} @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(''' - - - - - - ZZIRIX - сотовые аксессуары оптом - - - - - - -
-
-
- -

Каталог ZZIRIX

-
- -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - {% for category in categories %} - - {% endfor %} -
-
- -
-
- {% for product in products %} -
- - {% if product.get('photos') and product['photos']|length > 0 %} - {% set filename = product['photos'][0] %} - {% if filename.endswith(('.mp4', '.mov', '.webm')) %} - - {% else %} - {{ product['name'] }} - {% endif %} - {% else %} - Нет изображения - {% endif %} - -

{{ product['name'] }}

-
{{ "%.2f"|format(product['price']) }} с
-

{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}

-
- - -
-
- {% else %} -

Товаров пока нет. Загляните позже!

- {% endfor %} -
-
- - + products = data['products'] + categories = data['categories'] + + catalog_html = ''' + + + + + + ZZIRIX - сотовые аксессуары оптом + + + + - - -
-
-
- -

Каталог ZZIRIX

+ .category-filter { + font-size: 0.85rem; + padding: 8px 15px; + } + .products-grid { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + } + .product h2 { + font-size: 0.95rem; + } + .product-price { + font-size: 1.05rem; + } + .product-description { + font-size: 0.75rem; + } + .product-button { + font-size: 0.85rem; + padding: 8px; + } + #cart-button { + width: 45px; + height: 45px; + font-size: 1.1rem; + bottom: 15px; + right: 15px; + } + .modal-content { + margin: 20px auto; + padding: 20px; + max-height: calc(100vh - 40px); + } + .close { + font-size: 1.5rem; + top: 10px; + right: 15px; + } + .modal h2 { + font-size: 1.4rem; + } + .cart-item img { + width: 45px; + height: 45px; + } + } + + + +
+
+
+ +

Каталог ZZIRIX

+
+
- -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - {% for cat in categories %} - - {% endfor %} -
-
- -
-
- {% for product in products %} -
- + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ + {% for category in categories %} + + {% endfor %} +
+
+ +
+
+ {% for product in products %} +
{% if product.get('photos') and product['photos']|length > 0 %} - {% set filename = product['photos'][0] %} - {% if filename.endswith(('.mp4', '.mov', '.webm')) %} - - {% else %} - + {{ product['name'] }} - {% endif %} +
{% else %} - Нет изображения +
+ Нет изображения +
{% endif %} -
-

{{ product['name'] }}

-
{{ "%.2f"|format(product['price']) }} с
-

{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}

-
- - +

{{ product['name'] | escape }}

+
{{ "%.2f"|format(product['price']) }} с
+

{{ product['description'][:50] | escape }}{% if product['description']|length > 50 %}...{% endif %}

+
+ + +
+ {% endfor %}
- {% else %} -

В этой категории пока нет товаров.

- {% endfor %}
-
-