diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,3 @@ -from flask import Flask, render_template_string, request, redirect, url_for, send_file import json import os import logging @@ -8,7 +7,7 @@ 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 io +from flask import Flask, render_template_string, request, redirect, url_for app = Flask(__name__) DATA_FILE = 'data.json' @@ -17,31 +16,37 @@ REPO_ID = "Kgshop/zalkar" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -LOGO_URL = "https://cdn-avatars.huggingface.co/v1/production/uploads/67c280ccb9d3dfdee58ecfdd/4IIqefkClYrsa2euUC_Nh.jpeg" # Example logo URL, replace if needed +LOGO_URL = "https://cdn-avatars.huggingface.co/v1/production/uploads/67c280ccb9d3dfdee58ecfdd/4IIqefkClYrsa2euUC_Nh.jpeg" -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.DEBUG) def load_data(): try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - logging.info("Data successfully loaded from JSON.") + logging.info("Данные успешно загружены из JSON") if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: - # Attempt to handle older formats if they were just a list of categories - return {'products': [], 'categories': data if isinstance(data, list) else []} + # Handle cases where the data structure is not as expected + products = data if isinstance(data, list) else [] + categories = [] + # Attempt to find categories if products have a 'category' key + if products: + all_categories = {p.get('category') for p in products if p.get('category')} + categories = sorted(list(all_categories)) + return {'products': products, 'categories': categories} return data except FileNotFoundError: - logging.warning(f"Local database file '{DATA_FILE}' not found after download attempt. Starting with empty data.") + logging.warning("Локальный файл базы данных не найден после скачивания.") return {'products': [], 'categories': []} except json.JSONDecodeError: - logging.error(f"JSON decode error in '{DATA_FILE}'. Starting with empty data.") + logging.error("Ошибка: Невозможно декодировать JSON файл.") return {'products': [], 'categories': []} except RepositoryNotFoundError: - logging.error(f"Hugging Face repository '{REPO_ID}' not found. Starting with empty local database.") + logging.error("Репозиторий не найден. Создание локальной базы данных.") return {'products': [], 'categories': []} except Exception as e: - logging.error(f"An error occurred while loading data: {e}. Starting with empty data.") + logging.error(f"Произошла ошибка при загрузке данных: {e}") return {'products': [], 'categories': []} @@ -49,20 +54,16 @@ def save_data(data): try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) - logging.info(f"Data successfully saved to {DATA_FILE}") - # upload_db_to_hf() # Upload after successful save + logging.info("Данные успешно сохранены в JSON") + upload_db_to_hf() except Exception as e: - logging.error(f"Error saving data: {e}") - # Decide if you want to re-raise or handle silently + logging.error(f"Ошибка при сохранении данных: {e}") raise def upload_db_to_hf(): if not HF_TOKEN_WRITE or not REPO_ID: - logging.warning("Hugging Face write token or repo ID is not set. Skipping upload.") + logging.warning("HF_TOKEN_WRITE или REPO_ID не установлены. Пропуск загрузки резервной копии.") return - if not os.path.exists(DATA_FILE): - logging.warning(f"Local file '{DATA_FILE}' does not exist. Cannot upload.") - return try: api = HfApi() api.upload_file( @@ -71,57 +72,48 @@ def upload_db_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Automatic database backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("JSON database backup successfully uploaded to Hugging Face.") + logging.info("Резервная копия JSON базы успешно загр��жена на Hugging Face.") except Exception as e: - logging.error(f"Error uploading database backup: {e}") + logging.error(f"Ошибка при загрузке резервной копии: {e}") def download_db_from_hf(): - if not HF_TOKEN_READ or not REPO_ID: - logging.warning("Hugging Face read token or repo ID is not set. Skipping download.") - # Ensure a local file exists even if download fails initially - if not os.path.exists(DATA_FILE): - logging.warning(f"No local '{DATA_FILE}' and HF download skipped. Creating empty local file.") - save_data({'products': [], 'categories': []}) + if not REPO_ID or not (HF_TOKEN_READ or HF_TOKEN_WRITE): + logging.warning("REPO_ID и/или токены Hugging Face не установлены. Пропуск скачивания базы данных.") return try: - logging.info(f"Attempting to download {DATA_FILE} from {REPO_ID}...") hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", - token=HF_TOKEN_READ, + token=HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE, local_dir=".", local_dir_use_symlinks=False ) - logging.info("JSON database successfully downloaded from Hugging Face.") + logging.info("JSON база успешно скачана из Hugging Face.") except RepositoryNotFoundError as e: - logging.error(f"Hugging Face Repository not found: {e}") - # Decide how to handle: maybe create repo, or just proceed with local empty db - raise # Re-raise so load_data can handle it + logging.error(f"Репозиторий не найден: {e}") + # Decide if you want to create an empty local file or raise + if not os.path.exists(DATA_FILE): + logging.info(f"Создание пустого локального файла {DATA_FILE}.") + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump({'products': [], 'categories': []}, f) + raise # Re-raise to be caught by load_data except Exception as e: - logging.error(f"Error downloading JSON database: {e}") - # Decide how to handle: proceed with potentially old local file or empty db - raise # Re-raise so load_data can handle it - + logging.error(f"Ошибка при скачивании JSON базы: {e}") + raise # Re-raise to be caught by load_data def periodic_backup(): while True: - # Ensure the save_data function is called whenever data is modified, - # and save_data then calls upload_db_to_hf. - # This periodic task just acts as an extra safety net for any potential manual changes outside the app flow. - # Or, it can be the *only* upload mechanism if save_data doesn't upload immediately. - # Let's assume save_data handles the immediate upload. This can be a fallback. - logging.info("Performing periodic backup...") - upload_db_to_hf() # Call upload directly here for the periodic task - time.sleep(800) # Backup every 800 seconds (approx 13 minutes) + upload_db_to_hf() + time.sleep(800) @app.route('/') def catalog(): data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) + products = data['products'] + categories = data['categories'] catalog_html = ''' @@ -129,47 +121,50 @@ def catalog():
-
- {{ product['description'][:70] }}{% if product['description']|length > 70 %}...{% endif %}
- - +{{ product['description'] }}
+Загрузка...
'; // Loading state fetch('/product/' + index) .then(response => response.text()) .then(data => { - modalContentDiv.innerHTML = data; - // Re-initialize swiper if it was loaded - const swiperContainer = modalContentDiv.querySelector('.swiper-container'); - if(swiperContainer) { - initializeSwiper(swiperContainer); - } + document.getElementById('modalContent').innerHTML = data; + initializeSwiper(); }) - .catch(error => { - console.error('Error loading product details:', error); - modalContentDiv.innerHTML = 'Не удалось загрузить данные товара.
'; - }); + .catch(error => console.error('Ошибка загрузки деталей продукта:', error)); + } + + function initializeSwiper() { + // Check if a swiper container exists in the modal content + const swiperContainer = document.querySelector('#modalContent .swiper-container'); + if (swiperContainer) { + new Swiper(swiperContainer, { + slidesPerView: 1, + spaceBetween: 20, + loop: true, + grabCursor: true, + pagination: { el: '.swiper-pagination', clickable: true }, + navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }, + zoom: { maxRatio: 3 }, + // Optional: Add keyboard control or other features + }); + } } - function initializeSwiper(container) { - new Swiper(container, { - slidesPerView: 1, - spaceBetween: 20, - loop: true, - grabCursor: true, - autoplay: { - delay: 3000, - disableOnInteraction: false, - }, - pagination: { el: '.swiper-pagination', clickable: true }, - navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }, - zoom: { maxRatio: 3 }, // Enable zoom - }); - } function openQuantityModal(index) { selectedProductIndex = index; const product = products[index]; const colorSelect = document.getElementById('colorSelect'); - colorSelect.innerHTML = ''; // Clear previous options - if (product.colors && product.colors.length > 0 && product.colors.some(c => c.trim() !== '')) { + colorSelect.innerHTML = ''; + if (product.colors && product.colors.length > 0) { product.colors.forEach(color => { - if (color.trim() !== '') { - const option = document.createElement('option'); - option.value = color; - option.text = color; - colorSelect.appendChild(option); - } + const option = document.createElement('option'); + option.value = color; + option.text = color; + colorSelect.appendChild(option); }); } else { const option = document.createElement('option'); - option.value = 'Без цвета'; - option.text = 'Без цвета'; + option.value = 'Не указан'; // Change from 'Нет цвета' for clarity + option.text = 'Не указан'; colorSelect.appendChild(option); } document.getElementById('quantityModal').style.display = 'block'; @@ -890,30 +847,35 @@ def catalog(): function confirmAddToCart() { if (selectedProductIndex === null) return; - const quantity = parseFloat(document.getElementById('quantityInput').value); + const quantityInput = document.getElementById('quantityInput'); + const quantity = parseFloat(quantityInput.value); const color = document.getElementById('colorSelect').value; if (isNaN(quantity) || quantity <= 0) { alert("Укажите корректное количество метров больше 0"); + quantityInput.classList.add('input-error'); // Optional: Add a visual error state return; + } else { + quantityInput.classList.remove('input-error'); } + let cart = JSON.parse(localStorage.getItem('cart') || '[]'); const product = products[selectedProductIndex]; - // Use product ID or name + color for uniqueness - const cartItemId = `${product.name.replace(/\\s+/g, '_')}-${color}`; // Simple ID based on name and color + // Using product name and color for a unique ID + const cartItemId = `${product.name.replace(/\s+/g, '_')}-${color}`; // Use replace for potential spaces const existingItem = cart.find(item => item.id === cartItemId); if (existingItem) { - existingItem.quantity = parseFloat((existingItem.quantity + quantity).toFixed(2)); // Add quantity and fix precision + existingItem.quantity = parseFloat((existingItem.quantity + quantity).toFixed(2)); // Ensure precision } else { cart.push({ id: cartItemId, name: product.name, price: product.price, - photo: product.photos && product.photos.length > 0 ? product.photos[0] : null, // Store only the first photo filename - quantity: quantity, + photo: product.photos && product.photos.length > 0 ? product.photos[0] : '', + quantity: parseFloat(quantity.toFixed(2)), // Ensure precision color: color }); } @@ -921,19 +883,11 @@ def catalog(): localStorage.setItem('cart', JSON.stringify(cart)); closeModal('quantityModal'); updateCartButton(); - alert(`${product.name} (${quantity.toFixed(2)} метров, Цвет: ${color}) добавлен в корзину.`); // User feedback } function updateCartButton() { const cart = JSON.parse(localStorage.getItem('cart') || '[]'); - const cartButton = document.getElementById('cart-button'); - if (cart.length > 0) { - cartButton.style.display = 'flex'; // Use flex to center content - cartButton.classList.add('has-items'); - } else { - cartButton.style.display = 'none'; - cartButton.classList.remove('has-items'); - } + document.getElementById('cart-button').style.display = cart.length > 0 ? 'flex' : 'none'; // Use flex for centering icon } function openCartModal() { @@ -942,27 +896,28 @@ def catalog(): let total = 0; if (cart.length === 0) { - cartContent.innerHTML = 'Корзина пуста
'; + cartContent.innerHTML = 'Корзина пуста
'; } else { - cartContent.innerHTML = cart.map((item, index) => { + cartContent.innerHTML = cart.map(item => { const itemTotal = item.price * item.quantity; total += itemTotal; - const photoUrl = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60.png?text=No+Image'; + // Use placeholder image if photo is missing or invalid URL + const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60?text=No+Image'; + return `${item.price.toFixed(2)} $ × ${item.quantity.toFixed(2)} метров (Цвет: ${item.color})
-${item.price} $ × ${item.quantity.toFixed(2)} метров (Цвет: ${item.color})