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(''' + + +
+ + +{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
+Товаров пока нет. Загляните позже!
+ {% endfor %} +{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
+В этой категории пока нет товаров.
+ {% endfor %} +Категория: {{ product.get('category', 'Без категории') }}
+{{ "%.2f"|format(product['price']) }} с
+Описание: {{ product['description'] }}
+Доступные цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}
+Доступные модели: {{ product.get('models', ['Нет моделей'])|join(', ') }}
+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 %} +