diff --git "a/Soola.txt" "b/Soola.txt" new file mode 100644--- /dev/null +++ "b/Soola.txt" @@ -0,0 +1,2492 @@ + +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/testxse" # Using REPO_ID from app (2) (4).py for consistency with image paths in JSON +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', {}) + + # Ensure "Без категории" exists if there are products with it + 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', []) # Use 'photos' as per the provided JSON structure + product.setdefault('in_stock', True) # From provided JSON + product.setdefault('is_top', False) # From provided JSON + + # For compatibility, if 'media' was used, convert it to 'photos' for new system + # Or if 'photos' is empty, try to populate from 'media' + if not product['photos'] and 'media' in product and product['media']: + product['photos'] = [m['filename'] for m in product['media'] if m['type'] == 'photo'] + + # Remove old 'media' field if it exists, as 'photos' is now canonical + if 'media' in product: + del product['media'] + + # Sort categories to ensure "Без категории" is usually first or consistently placed + 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, #f8f9fa, #e9ecef); + --background-dark: linear-gradient(135deg, #1a202c, #2d3748); + --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 6px 20px rgba(0, 0, 0, 0.08); + --shadow-hover-light: 0 10px 30px rgba(0, 0, 0, 0.15); + --shadow-dark: 0 6px 20px rgba(0, 0, 0, 0.25); + --shadow-hover-dark: 0 10px 30px rgba(0, 0, 0, 0.4); +} +* { + 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: 1300px; + margin: 0 auto; + padding: 20px; + flex-grow: 1; +} +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 0; + border-bottom: 1px solid var(--border-color-light); + margin-bottom: 20px; +} +body.dark-mode .header { + border-bottom-color: var(--border-color-dark); +} +.header-info { + display: flex; + align-items: center; +} +.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; +} +.header-logo:hover { + transform: scale(1.05); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} +.header h1 { + font-size: 1.7rem; + font-weight: 700; + margin-left: 15px; +} +.theme-toggle { + background: none; + border: none; + font-size: 1.6rem; + 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(--primary-color); + transform: rotate(15deg); +} + +.flash { + padding: 15px; + margin-bottom: 20px; + border-radius: 8px; + font-weight: 500; + border: 1px solid transparent; +} +.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: 20px 0; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; +} +.search-container { + margin: 20px 0; + text-align: center; +} +#search-input { + width: 90%; + max-width: 600px; + padding: 12px 18px; + font-size: 1rem; + border: 1px solid var(--border-color-light); + border-radius: 25px; + outline: none; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); + 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); +} +#search-input:focus { + border-color: var(--primary-color); + box-shadow: 0 0 8px rgba(59, 130, 246, 0.3); +} +.category-filter { + padding: 10px 20px; + border: 1px solid var(--border-color-light); + border-radius: 25px; + background-color: var(--card-background-light); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 0.95rem; + 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(180px, 1fr)); + gap: 20px; + padding: 10px; +} +.product { + background: var(--card-background-light); + border-radius: 18px; + padding: 15px; + 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(-8px) scale(1.02); + 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: #f0f0f0; + border-radius: 12px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 10px; +} +body.dark-mode .product-image { + background-color: #3a4250; +} +.product-image img, .product-image video { + max-width: 100%; + max-height: 100%; + object-fit: contain; + transition: transform 0.3s ease; +} +.product-image img:hover, .product-image video:hover { + transform: scale(1.05); +} +.product h2 { + font-size: 1.05rem; + font-weight: 600; + margin: 5px 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.15rem; + color: var(--danger-color); + font-weight: 700; + text-align: center; + margin: 5px 0 10px; +} +.product-description { + font-size: 0.85rem; + color: var(--secondary-text-color-light); + text-align: center; + margin-bottom: 15px; + 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: 8px; + margin-top: auto; +} +.product-button { + display: block; + width: 100%; + padding: 10px; + border: none; + border-radius: 10px; + background-color: var(--primary-color); + color: white; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-align: center; + text-decoration: none; +} +.product-button:hover { + background-color: var(--primary-dark-color); + box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4); +} +.add-to-cart { + background-color: var(--accent-color); +} +.add-to-cart:hover { + background-color: var(--accent-dark-color); + box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4); +} +#cart-button { + position: fixed; + bottom: 25px; + right: 25px; + background-color: var(--danger-color); + color: white; + border: none; + border-radius: 50%; + width: 55px; + height: 55px; + font-size: 1.3rem; + cursor: pointer; + box-shadow: 0 5px 20px rgba(239, 68, 68, 0.4); + 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(-3px) scale(1.05); + box-shadow: 0 8px 25px rgba(239, 68, 68, 0.6); +} +.cart-count { + position: absolute; + top: -5px; + right: -5px; + background-color: var(--accent-color); + color: white; + border-radius: 50%; + padding: 3px 7px; + font-size: 0.75rem; + min-width: 20px; + 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: 30px; + border-radius: 20px; + width: 95%; + max-width: 750px; + box-shadow: 0 15px 40px rgba(0,0,0,0.3); + animation: fadeInScale 0.3s ease-out; + max-height: calc(100vh - 100px); + 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: 0 15px 40px rgba(0,0,0,0.5); +} +@keyframes fadeInScale { + from { opacity: 0; transform: translateY(-30px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +.close { + position: absolute; + top: 15px; + right: 20px; + font-size: 1.8rem; + 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.6rem; + font-weight: 700; + margin-bottom: 20px; + text-align: center; +} +.cart-item { + display: flex; + align-items: center; + padding: 15px 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: 60px; + height: 60px; + object-fit: contain; + border-radius: 10px; + margin-right: 15px; + background-color: #f0f0f0; +} +body.dark-mode .cart-item img { + background-color: #3a4250; +} +.cart-item-details { + flex-grow: 1; +} +.cart-item-details strong { + font-size: 1.1rem; + font-weight: 600; +} +.cart-item-details p { + font-size: 0.9rem; + color: var(--secondary-text-color-light); +} +body.dark-mode .cart-item-details p { + color: var(--secondary-text-color-dark); +} +.cart-item-total { + font-size: 1rem; + font-weight: 700; + color: var(--danger-color); +} +.quantity-input, .color-select, .model-select { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color-light); + border-radius: 8px; + font-size: 1rem; + margin: 8px 0; + background-color: var(--card-background-light); + color: var(--text-color-light); +} +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); +} +.modal-buttons { + margin-top: 20px; + display: flex; + justify-content: flex-end; + gap: 10px; +} +.modal-buttons .product-button { + width: auto; + padding: 10px 20px; +} +.clear-cart { + background-color: var(--danger-color); +} +.clear-cart:hover { + background-color: var(--danger-dark-color); + box-shadow: 0 4px 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 4px 15px rgba(16, 185, 129, 0.4); +} + +.swiper-container { + max-width: 400px; + margin: 0 auto 20px; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 5px 20px rgba(0,0,0,0.1); +} +body.dark-mode .swiper-container { + box-shadow: 0 5px 20px rgba(0,0,0,0.3); +} +.swiper-slide { + background-color: #f0f0f0; + display: flex; + justify-content: center; + align-items: center; + min-height: 250px; +} +body.dark-mode .swiper-slide { + background-color: #3a4250; +} +.swiper-slide img, .swiper-slide video { + max-width: 100%; + max-height: 300px; + object-fit: contain; +} +.swiper-button-next, .swiper-button-prev { + color: var(--primary-color) !important; + background-color: rgba(255,255,255,0.8); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s; +} +.swiper-button-next:hover, .swiper-button-prev:hover { + background-color: rgba(255,255,255,1); +} +body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev { + background-color: rgba(45, 55, 72, 0.8); +} +body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover { + background-color: rgba(45, 55, 72, 1); +} +.swiper-pagination-bullet { + background-color: var(--primary-color) !important; +} + +.product-detail-page { + background: var(--card-background-light); + margin: 50px auto; + padding: 30px; + border-radius: 20px; + width: 95%; + max-width: 750px; + box-shadow: 0 15px 40px rgba(0,0,0,0.3); + position: relative; +} +body.dark-mode .product-detail-page { + background: var(--card-background-dark); + color: var(--text-color-dark); + box-shadow: 0 15px 40px rgba(0,0,0,0.5); +} +.product-detail-page h1 { + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 25px; + text-align: center; + color: var(--text-color-light); +} +body.dark-mode .product-detail-page h1 { + color: var(--text-color-dark); +} +.product-detail-page p { + margin-bottom: 10px; + font-size: 1rem; +} +.product-detail-page p strong { + color: var(--text-color-light); +} +body.dark-mode .product-detail-page p strong { + color: var(--text-color-dark); +} +.product-detail-page .price { + font-size: 1.8rem; + color: var(--danger-color); + font-weight: 700; + margin-bottom: 20px; + text-align: center; +} +.product-detail-page .description { + margin-bottom: 20px; + white-space: pre-wrap; +} +.product-detail-actions { + display: flex; + justify-content: center; + gap: 15px; + margin-top: 30px; +} +.product-detail-actions .product-button { + width: auto; + padding: 12px 25px; +} +.back-to-catalog { + margin-bottom: 20px; + text-align: left; +} + + +@media (max-width: 768px) { + .header h1 { + font-size: 1.4rem; + } + .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; + } + .product-detail-page { + margin: 20px auto; + padding: 20px; + max-height: calc(100vh - 40px); + } + .product-detail-page h1 { + font-size: 1.8rem; + } + .product-detail-page .price { + font-size: 1.5rem; + } + .product-detail-actions { + flex-direction: column; + gap: 10px; + } +} +''' + +@app.route('/') +def catalog(): + data = load_data() + products = data.get('products', []) + categories = data.get('categories', []) + + 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=products, + categories=categories, + repo_id=REPO_ID, + LOGO_URL=LOGO_URL + ) + +@app.route('/category/') +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()] + + return render_template_string(''' + + + + + + {{ category_name }} - ZZIRIX + + + + + + +
+
+
+ +

Каталог 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 %} +
+ + {% 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=products, + categories=categories, + category_name=category_name, + products_all=products_all, # Pass all products for JS lookup + repo_id=REPO_ID, + LOGO_URL=LOGO_URL + ) + +@app.route('/product/') +def product_page(product_id): + data = load_data() + product = next((p for p in data.get('products', []) if p.get('id') == product_id), None) + categories = data.get('categories', []) + + if product is None: + flash('Товар не найден.', 'error') + return redirect(url_for('catalog')) + + return render_template_string(''' + + + + + + {{ product.name }} - ZZIRIX + + + + + + +
+
+
+ +

Каталог ZZIRIX

+
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+

{{ product['name'] }}

+
+
+ {% if product.get('photos') %} + {% for photo in product['photos'] %} +
+ {% if photo.endswith(('.mp4', '.mov', '.webm')) %} + + {% else %} + {{ product['name'] }} + {% endif %} +
+ {% endfor %} + {% else %} +
+ No Image +
+ {% endif %} +
+
+
+
+
+

Категория: {{ product.get('category', 'Без категории') }}

+

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

+

Описание: {{ product['description'] }}

+

Доступные цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}

+

Доступные модели: {{ product.get('models', ['Нет моделей'])|join(', ') }}

+
+ +
+
+
+ + + + + + + + + + + + ''', + product=product, + categories=categories, + repo_id=REPO_ID, + LOGO_URL=LOGO_URL, + data=data # Pass full data for JS to get all products + ) + +@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) + + # Delete old photos from HF if they exist + 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: + # Delete associated photos from HF + 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')) + + return render_template_string(''' + + + + + + Админ-панель ZZIRIX + + + + +
+
+
+ +

Админ-панель

+
+ Перейти в каталог +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +

Добавление товара

+
+ + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Управление категориями

+
+ + + + +
+ +

Список категорий

+
+ {% for category in categories %} +
+

{{ category }}

+
+ + + +
+
+ {% endfor %} +
+ +

Управление базой данных

+
+
+ +
+
+ +
+
+ +

Список товаров

+
+ +
+
+ {% for product in products %} +
+

{{ product['name'] }}

+

ID: {{ product.id }}

+

Категория: {{ product.get('category', 'Без категории') }}

+

Цена: {{ "%.2f"|format(product['price']) }} с

+

Описание: {{ product['description'] }}

+

Цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}

+

Модели: {{ product.get('models', ['Нет моделей'])|join(', ') }}

+ {% if product.get('photos') and product['photos']|length > 0 %} +
+ {% for photo in product['photos'] %} + {% if photo.endswith(('.mp4', '.mov', '.webm')) %} + + {% else %} + {{ product['name'] }} + {% endif %} + {% endfor %} +
+ {% endif %} +
+ Редактировать +
+ + + + + + + + + + + + + +
+ {% for color in product.get('colors', []) %} +
+ + +
+ {% endfor %} +
+ + +
+
+ + +
+ {% for model in product.get('models', []) %} +
+ + +
+ {% endfor %} +
+ + +
+
+ + +
+
+
+ + + +
+
+ {% endfor %} +
+
+ + + + ''', + 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() # Ensure latest data is loaded before saving/uploading + 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() # Ensure the latest version is downloaded before sending + 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) # This will save the initialized structure and upload to HF + 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)