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(): - Zalkar Textile - ткани на заказ оптом + Zalkar Textile - ткани на заказ - +
-
- -

Zalkar Textile

-
+ +

Zalkar Textile

@@ -744,15 +711,17 @@ def catalog(): loading="lazy">
{% else %} -
- No Image Placeholder -
+
+ No image available +
{% endif %}

{{ product['name'] }}

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

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

- - +
{{ product['price'] }} $
+

{{ product['description'] }}

+
+ + +
{% endfor %} @@ -768,13 +737,12 @@ def catalog(): @@ -783,12 +751,12 @@ def catalog(): ×

Корзина

- @@ -805,21 +773,19 @@ def catalog(): function toggleTheme() { document.body.classList.toggle('dark-mode'); const icon = document.querySelector('.theme-toggle i'); - const isDarkMode = document.body.classList.contains('dark-mode'); - icon.classList.remove('fa-moon', 'fa-sun'); - icon.classList.add(isDarkMode ? 'fa-sun' : 'fa-moon'); - localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); + icon.classList.toggle('fa-moon'); + icon.classList.toggle('fa-sun'); + localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light'); } - // Apply theme on load if (localStorage.getItem('theme') === 'dark') { document.body.classList.add('dark-mode'); - document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun'); - } else { - document.querySelector('.theme-toggle i').classList.add('fa-moon'); // Ensure correct icon if not dark + const icon = document.querySelector('.theme-toggle i'); + if(icon) { // Check if icon exists before replacing class + icon.classList.replace('fa-moon', 'fa-sun'); + } } - function openModal(index) { loadProductDetails(index); document.getElementById('productModal').style.display = "block"; @@ -830,58 +796,49 @@ def catalog(): } function loadProductDetails(index) { - const modalContentDiv = document.getElementById('modalContent'); - modalContentDiv.innerHTML = '

Загрузка...

'; // 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.name} -
-

${item.name}

-

${item.price.toFixed(2)} $ × ${item.quantity.toFixed(2)} метров (Цвет: ${item.color})

-
+ ${item.name} +
+ ${item.name} +

${item.price} $ × ${item.quantity.toFixed(2)} метров (Цвет: ${item.color})

- ${itemTotal.toFixed(2)} $ + ${itemTotal.toFixed(2)} $
`; }).join(''); } + document.getElementById('cartTotal').textContent = total.toFixed(2); document.getElementById('cartModal').style.display = 'block'; } @@ -970,47 +925,45 @@ def catalog(): function orderViaWhatsApp() { const cart = JSON.parse(localStorage.getItem('cart') || '[]'); if (cart.length === 0) { - alert("Корзина пуста! Нечего заказывать."); + alert("Корзина пуста!"); return; } let total = 0; - let orderText = "Новый заказ с сайта:%0A%0A"; // Add intro text + let orderText = "Заказ:%0A"; // %0A is newline for WhatsApp URL encoding cart.forEach((item, index) => { const itemTotal = item.price * item.quantity; total += itemTotal; - orderText += `${index + 1}. ${item.name}%0A`; - orderText += ` Кол-во: ${item.quantity.toFixed(2)} метров%0A`; // Specify meters - orderText += ` Цвет: ${item.color}%0A`; - orderText += ` Цена/метр: ${item.price.toFixed(2)} $%0A`; // Specify price per meter - orderText += ` Итого за позицию: ${itemTotal.toFixed(2)} $%0A%0A`; // Item subtotal + orderText += `${index + 1}. ${item.name} - ${item.price} $ × ${item.quantity.toFixed(2)} метров (Цвет: ${item.color})%0A`; }); - orderText += `Общая сумма заказа: ${total.toFixed(2)} $`; // Total order amount + orderText += `Итого: ${total.toFixed(2)} $`; - // Construct WhatsApp URL. Replace 996707842888 with the actual number if different. - const whatsappNumber = '996707842888'; - const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodeURIComponent(orderText)}`; - - window.open(whatsappUrl, '_blank'); - - // Optionally clear the cart after sending the order - // clearCart(); - // alert("Ваш заказ отправлен в WhatsApp. Скоро с вами свяжутся."); + // Ensure the phone number is correct + const phoneNumber = "996707842888"; + window.open(`https://api.whatsapp.com/send?phone=${phoneNumber}&text=${orderText}`, '_blank'); } - function clearCart() { - if (confirm("Вы уверены, что хотите очистить корзину?")) { + if (confirm("Вы уверены, что хотите очистить корзину?")) { localStorage.removeItem('cart'); closeModal('cartModal'); updateCartButton(); - alert("Корзина очищена."); - } + } } - // Close modals when clicking outside window.onclick = function(event) { - if (event.target.classList.contains('modal')) { - event.target.style.display = "none"; + // Close modals if clicked outside the modal content + const productModal = document.getElementById('productModal'); + const quantityModal = document.getElementById('quantityModal'); + const cartModal = document.getElementById('cartModal'); + + if (event.target === productModal) { + productModal.style.display = "none"; + } + if (event.target === quantityModal) { + quantityModal.style.display = "none"; + } + if (event.target === cartModal) { + cartModal.style.display = "none"; } } @@ -1027,20 +980,31 @@ def catalog(): const searchTerm = document.getElementById('search-input').value.toLowerCase(); const activeCategory = document.querySelector('.category-filter.active').dataset.category; document.querySelectorAll('.product').forEach(product => { - const name = product.getAttribute('data-name'); - const description = product.getAttribute('data-description'); - const category = product.getAttribute('data-category'); + const name = product.getAttribute('data-name') || ''; // Handle potential missing attribute + const description = product.getAttribute('data-description') || ''; // Handle potential missing attribute + const category = product.getAttribute('data-category') || ''; // Handle potential missing attribute + const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm); - const matchesCategory = activeCategory === 'all' || category === activeCategory; - product.style.display = matchesSearch && matchesCategory ? 'flex' : 'none'; // Use flex display for product items + const matchesCategory = activeCategory === 'all' || category.toLowerCase() === activeCategory.toLowerCase(); // Case-insensitive category match + + product.style.display = matchesSearch && matchesCategory ? 'flex' : 'none'; // Use flex as product is flex container }); } - // Initial call to set up the cart button state + // Initialize the cart button display on page load updateCartButton(); - // Initial filter application + // Initial filter application filterProducts(); + // Add event listener for closing modals with Escape key + document.addEventListener('keydown', function(event) { + if (event.key === "Escape") { + closeModal('productModal'); + closeModal('quantityModal'); + closeModal('cartModal'); + } + }); + @@ -1050,59 +1014,48 @@ def catalog(): @app.route('/product/') def product_detail(index): data = load_data() - products = data.get('products', []) + products = data['products'] try: product = products[index] - except (IndexError, TypeError): # Handle cases where products might not be a list or index is out of bounds - return "

Продукт не найден

", 404 - + except IndexError: + return "Продукт не найден", 404 detail_html = ''' -
-

{{ product['name'] }}

- {% if product.get('photos') and product['photos']|length > 0 %} -
+
+

{{ product['name'] }}

+
+ {% if product.get('photos') %} {% for photo in product['photos'] %} -
- {{ product['name'] }} +
+
+ {{ product['name'] }} +
{% endfor %} -
-
- {% if product['photos']|length > 1 %} {# Only show navigation if more than one photo #} -
-
- {% endif %} -
- {% else %} -
- No Image Placeholder -
- {% endif %} - -
-

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

-

Цена за метр: {{ "%.2f"|format(product.get('price', 0)) }} $

{# Ensure price is float and formatted #} -

Описание: {{ product.get('description', 'Нет описания') }}

-

Доступные цвета: - {% if product.get('colors') and product['colors']|length > 0 %} - {{ product['colors']|join(', ') }} {% else %} - Нет цветов +

+ No Image +
{% endif %} -

+
+
+
+
+

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

+

Цена: {{ product['price'] }} $

+

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

+

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

''' - # Ensure price is treated as float for formatting - product['price'] = float(product.get('price', 0)) return render_template_string(detail_html, product=product, repo_id=REPO_ID) @app.route('/admin', methods=['GET', 'POST']) def admin(): data = load_data() - products = data.get('products', []) + products = data.get('products', []) # Ensure lists exist categories = data.get('categories', []) if request.method == 'POST': @@ -1112,60 +1065,58 @@ def admin(): category_name = request.form.get('category_name', '').strip() if category_name and category_name not in categories: categories.append(category_name) + categories.sort() # Keep categories sorted + data['categories'] = categories # Update data dict save_data(data) - upload_db_to_hf() return redirect(url_for('admin')) - # Handle invalid input or duplicate category - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка: Категория уже существует или не указано название.") + return "Ошибка: Категория уже существует или не указано название", 400 elif action == 'delete_category': try: - category_name = request.form.get('category_name') # Get name instead of index for safety - if category_name in categories: - categories.remove(category_name) - # Assign 'Без категории' to products in the deleted category - for product in products: - if product.get('category') == category_name: - product['category'] = 'Без категории' - save_data(data) - upload_db_to_hf() - return redirect(url_for('admin')) - else: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка: Категория не найдена.") - except Exception as e: - logging.error(f"Error deleting category: {e}") - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error=f"Ошибка при удалении категории: {e}") + category_index = int(request.form.get('category_index')) + if 0 <= category_index < len(categories): + deleted_category = categories.pop(category_index) + data['categories'] = categories # Update data dict + # Reassign products in the deleted category + for product in products: + if product.get('category') == deleted_category: + product['category'] = 'Без категории' + data['products'] = products # Update data dict + save_data(data) + return redirect(url_for('admin')) + return "Ошибка: Неверный индекс категории", 400 + except ValueError: + return "Ошибка: Неверный индекс категории", 400 elif action == 'add': name = request.form.get('name', '').strip() - price_str = request.form.get('price', '').strip() + price_str = request.form.get('price', '0').replace(',', '.') description = request.form.get('description', '').strip() - category = request.form.get('category', 'Без категории').strip() + category = request.form.get('category') photos_files = request.files.getlist('photos') colors = request.form.getlist('colors') + photos_list = [] if not name or not price_str or not description: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка: Заполните все обязательные поля (Название, Цена, Описание).") + return "Ошибка: Заполните все обязательные поля", 400 try: - price = float(price_str.replace(',', '.')) - if price < 0: price = 0 + price = float(price_str) + if price < 0: + raise ValueError("Цена не может быть отрицательной") except ValueError: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка: Некорректное значение цены.") + return "Ошибка: Неверный формат цены", 400 - photos_list = [] - if photos_files and any(photo.filename for photo in photos_files): - api = HfApi(token=HF_TOKEN_WRITE) + if photos_files: uploads_dir = 'uploads' os.makedirs(uploads_dir, exist_ok=True) - upload_count = 0 - for photo in photos_files: - if photo and photo.filename and upload_count < 10: # Limit to 10 photos + api = HfApi(token=HF_TOKEN_WRITE) # Pass token explicitly + for photo in photos_files[:10]: # Limit to 10 photos + if photo and photo.filename: original_filename = secure_filename(photo.filename) - # Create a more unique filename to prevent conflicts - timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") - photo_filename = f"{timestamp}_{original_filename}" + # Prevent overwriting files with the same name if uploaded at different times + photo_filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{original_filename}" temp_path = os.path.join(uploads_dir, photo_filename) try: photo.save(temp_path) @@ -1174,193 +1125,128 @@ def admin(): path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", - commit_message=f"Added photo for product {name}" + token=HF_TOKEN_WRITE, + commit_message=f"Добавлено фото {photo_filename} для товара {name}" ) photos_list.append(photo_filename) - upload_count += 1 - logging.info(f"Uploaded photo: {photo_filename}") - except Exception as upload_error: - logging.error(f"Error uploading photo {original_filename}: {upload_error}") - # Decide whether to continue or stop on error + except Exception as e: + logging.error(f"Ошибка загрузки фото {original_filename} на HF: {e}") + # Optionally return an error or log and continue finally: if os.path.exists(temp_path): - os.remove(temp_path) # Clean up temp file + os.remove(temp_path) # Clean up temporary file new_product = { 'name': name, 'price': price, 'description': description, - 'category': category if category in categories or category == 'Без категории' else 'Без категории', + 'category': category if category in categories else 'Без категории', 'photos': photos_list, - 'colors': [c.strip() for c in colors if c.strip()] if colors else [] + 'colors': [c.strip() for c in colors if c.strip()] # Clean up color strings } products.append(new_product) + data['products'] = products # Update data dict save_data(data) - upload_db_to_hf() return redirect(url_for('admin')) elif action == 'edit': try: index = int(request.form.get('index')) - if 0 <= index < len(products): - name = request.form.get('name', '').strip() - price_str = request.form.get('price', '').strip() - description = request.form.get('description', '').strip() - category = request.form.get('category', 'Без категории').strip() - photos_files = request.files.getlist('photos') - colors = request.form.getlist('colors') - - if not name or not price_str or not description: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка при редактировании: Заполните все обязательные поля.") - - try: - price = float(price_str.replace(',', '.')) - if price < 0: price = 0 - except ValueError: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка при редактировании: Некорректное значение цены.") - - - # Handle photo updates - if photos_files and any(photo.filename for photo in photos_files): - # Option: Delete old photos first (more complex, requires tracking old filenames) - # For simplicity here, we just upload new ones and update the list. - # Old files on HF repo will remain unless manually deleted. - api = HfApi(token=HF_TOKEN_WRITE) - uploads_dir = 'uploads' - os.makedirs(uploads_dir, exist_ok=True) - new_photos_list = [] - upload_count = 0 - for photo in photos_files: - if photo and photo.filename and upload_count < 10: # Limit to 10 photos - original_filename = secure_filename(photo.filename) - timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") - photo_filename = f"{timestamp}_{original_filename}" - temp_path = os.path.join(uploads_dir, photo_filename) - try: - photo.save(temp_path) - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=f"photos/{photo_filename}", - repo_id=REPO_ID, - repo_type="dataset", - commit_message=f"Updated photo for product {name}" - ) - new_photos_list.append(photo_filename) - upload_count += 1 - logging.info(f"Uploaded new photo: {photo_filename}") - except Exception as upload_error: - logging.error(f"Error uploading photo {original_filename}: {upload_error}") - finally: - if os.path.exists(temp_path): - os.remove(temp_path) - - products[index]['photos'] = new_photos_list # Replace old photos with new ones - # else: If no new files are uploaded, the old photos list remains unchanged. - - products[index]['name'] = name - products[index]['price'] = price - products[index]['description'] = description - products[index]['category'] = category if category in categories or category == 'Без категории' else 'Без категории' - products[index]['colors'] = [c.strip() for c in colors if c.strip()] if colors else [] - - save_data(data) - upload_db_to_hf() - return redirect(url_for('admin')) - else: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка при редактировании: Некорректный индекс товара.") + if not (0 <= index < len(products)): + return "Ошибка: Неверный индекс товара", 400 + + product = products[index] + name = request.form.get('name', '').strip() + price_str = request.form.get('price', '0').replace(',', '.') + description = request.form.get('description', '').strip() + category = request.form.get('category') + photos_files = request.files.getlist('photos') + colors = request.form.getlist('colors') + + if not name or not price_str or not description: + return "Ошибка: Заполните все обязательные поля при редактировании", 400 + + try: + price = float(price_str) + if price < 0: + raise ValueError("Цена не может быть отрицательной") + except ValueError: + return "Ошибка: Неверный формат цены при редактировании", 400 + + product['name'] = name + product['price'] = price + product['description'] = description + product['category'] = category if category in categories else 'Без категории' + product['colors'] = [c.strip() for c in colors if c.strip()] # Clean up color strings + + # Handle photo updates only if new photos are provided + if photos_files and any(photo.filename for photo in photos_files): + new_photos_list = [] + uploads_dir = 'uploads' + os.makedirs(uploads_dir, exist_ok=True) + api = HfApi(token=HF_TOKEN_WRITE) # Pass token explicitly + for photo in photos_files[:10]: # Limit to 10 photos + if photo and photo.filename: + original_filename = secure_filename(photo.filename) + photo_filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{original_filename}" + temp_path = os.path.join(uploads_dir, photo_filename) + try: + photo.save(temp_path) + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=f"photos/{photo_filename}", + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Обновлено фото {photo_filename} для товара {name}" + ) + new_photos_list.append(photo_filename) + except Exception as e: + logging.error(f"Ошибка загрузки обновленного фото {original_filename} на HF: {e}") + finally: + if os.path.exists(temp_path): + os.remove(temp_path) + + # Replace old photos list only if new photos were successfully uploaded + if new_photos_list: + product['photos'] = new_photos_list + + data['products'] = products # Update data dict + save_data(data) + return redirect(url_for('admin')) except ValueError: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка при редактировании: Некорректные данные индекса.") - except Exception as e: - logging.error(f"Error editing product: {e}") - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error=f"Произошла ошибка при редактировании товара: {e}") + return "Ошибка: Неверный индекс товара", 400 elif action == 'delete': try: index = int(request.form.get('index')) if 0 <= index < len(products): - # Optional: Add logic here to delete associated photos from HF repo del products[index] + data['products'] = products # Update data dict save_data(data) - upload_db_to_hf() return redirect(url_for('admin')) - else: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка при удалении: Некорректный индекс товара.") + return "Ошибка: Неверный индекс товара", 400 except ValueError: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Ошибка при удалении: Некорректные данные индекса.") - except Exception as e: - logging.error(f"Error deleting product: {e}") - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error=f"Произошла ошибка при удалении товара: {e}") - - elif action == 'upload_db': - try: - upload_db_to_hf() - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, success="База данных успешно загружена на Hugging Face.") - except Exception as e: - logging.error(f"Manual DB upload failed: {e}") - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error=f"Ошибка при загрузке базы данных: {e}") - - elif action == 'download_db': - try: - download_db_from_hf() - data = load_data() # Reload data after download - products = data.get('products', []) - categories = data.get('categories', []) - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, success="База данных успешно скачана с Hugging Face.") - except Exception as e: - logging.error(f"Manual DB download failed: {e}") - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error=f"Ошибка при скачивании базы данных: {e}") - - elif action == 'view_db': - # Serve the JSON file directly - try: - return send_file(DATA_FILE, as_attachment=True, download_name=DATA_FILE) - except FileNotFoundError: - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error="Файл базы данных не найден.") - except Exception as e: - logging.error(f"Error serving DB file: {e}") - return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, error=f"Ошибка при предоставлении файла базы данных: {e}") - - + return "Ошибка: Неверный индекс товара", 400 + admin_html = ''' - Админ-панель Zalkar Textile - + Админ-панель + -
-
- - -

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

-
+
+ +

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

- {% if error %} -
{{ error }}
- {% endif %} - {% if success %} -
{{ success }}
- {% endif %} - -

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

-
-
- - - - - - - - - - - - +

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

+ + + + - - + + - -
-
- - -
-
- + + - -
-
+ + -

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

-
-
- - - - -
+ + -

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

- {% if categories %} -
- {% for category in categories %} -
- {{ category }} -
- - {# Use name for deletion #} - -
+ +
+
+ +
- {% endfor %}
- {% else %} -

Категории пока не добавлены.

- {% endif %} + + + + + +

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

+
+ + + + +
+ +

Список категорий ({{ categories|length }})

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

{{ category }}

+
+ + + +
+
+ {% endfor %} + {% if not categories %} +

Нет добавленных категорий.

+ {% endif %}
-

Управление базой данных (data.json)

-
-

База данных хранится на Hugging Face в репозитории {{ repo_id }}

-
-
- - -
-
- - -
-
- - -
-
+

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

+
+
+ +
+
+ +

Список товаров ({{ products|length }})

- {% if products %}
{% for product in products %}
-

{{ product['name'] }}

+

{{ product.get('name', 'Без названия') }}

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

-

Цена за метр: {{ "%.2f"|format(product.get('price', 0)) }} $

-

Описание: {{ product.get('description', 'Нет описания') }}

-

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

+

Цена: {{ product.get('price', 0)|float|round(2) }} $

+

Описание: {{ product.get('description', 'Без оп��сания') }}

+

Цвета: {{ product.get('colors', ['Не указаны'])|join(', ') }}

{% if product.get('photos') and product['photos']|length > 0 %} -
+
{% for photo in product['photos'] %} - {% set photo_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/photos/" + photo %} - Фото товара {% endfor %}
@@ -1758,56 +1618,57 @@ def admin():
- - - - - - - - + + + + + + + + + - - -

Текущие фото будут заменены на выбранные.

- + + + +
{% for color in product.get('colors', []) %} - {% if color.strip() %}
- - + +
- {% endif %} {% endfor %} - {% if not product.get('colors') or not product['colors']|select('trim')|list %} {# If colors list is empty or contains only whitespace #} + {% if not product.get('colors') %}
- +
{% endif %}
- + - +
-
+ - +
{% endfor %} + {% if not products %} +

Нет добавленных товаров.

+ {% endif %}
- {% else %} -

Товары пока не добавлены.

- {% endif %}