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('''
{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
Товаров пока нет. Загляните позже!
{% endfor %}{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
В этой категории пока нет товаров.
{% endfor %}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 %}