diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,7 @@ -from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify + + + +from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify, session import json import os import logging @@ -15,7 +18,7 @@ import uuid load_dotenv() app = Flask(__name__) -app.secret_key = 'dallarsi_secret_key_no_login' +app.secret_key = 'dalarssi_secret_key_no_login' DATA_FILE = 'data.json' @@ -35,6 +38,137 @@ DOWNLOAD_DELAY = 5 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +translations = { + 'ru': { + 'site_title': "dalarssi - Каталог", + 'brand_name': "dalarssi", + 'our_address': "Наш адрес:", + 'all_categories': "Все категории", + 'search_placeholder': "Поиск по названию или описанию...", + 'no_products_added': "Товары пока не добавлены.", + 'no_products_found': "По вашему запросу товары не найдены.", + 'top_product': "Топ", + 'price_per_line': "Цена за линейку", + 'price_per_piece': "Цена за шт.", + 'items_in_line': "шт. в линейке", + 'details': "Подробнее", + 'add_to_cart': "В корзину", + 'loading': "Загрузка...", + 'specify_quantity_color': "Укажите количество и цвет", + 'quantity': "Количество (линеек):", + 'color_variant': "Цвет/Вариант:", + 'confirm_add_to_cart': "Добавить в корзину", + 'your_cart': "Ваша корзина", + 'cart_is_empty': "Ваша корзина пуста.", + 'total': "Итого:", + 'clear_cart': "Очистить корзину", + 'formulate_order': "Сформировать заказ", + 'open_cart': "Открыть корзину", + 'close': "Закрыть", + 'product_added_notification': "добавлен в корзину!", + 'product_load_error': "Не удалось загрузить информацию о товаре.", + 'enter_correct_quantity': "Пожалуйста, укажите корректное количество (больше 0).", + 'product_add_error': "Ошибка добавления: товар не найден.", + 'confirm_clear_cart': "Вы уверены, что хотите очистить корзину?", + 'cart_empty_error': "Корзина пуста! Добавьте товары перед формированием заказа.", + 'formulating_order': "Формируем заказ...", + 'order_creation_error': "Ошибка при формировании заказа:", + 'category': "Категория:", + 'no_category': "Без категории", + 'price': "Цена:", + 'description': "Описание:", + 'no_description': "Описание отсутствует.", + 'available_colors': "Доступные цвета/варианты:", + 'order_title': "Заказ №", + 'your_order': "Ваш Заказ №", + 'creation_date': "Дата создания:", + 'products_in_order': "Товары в заказе", + 'total_to_pay': "ИТОГО К ОПЛАТЕ:", + 'order_status': "Статус заказа", + 'order_status_desc': "Этот заказ был оформлен без входа в систему. Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.", + 'send_order': "Отправить заказ", + 'back_to_catalog': "← Вернуться в каталог", + 'whatsapp_greeting': "Здравствуйте! Хочу подтвердить свой заказ на dalarssi:", + 'whatsapp_order_number': "Номер заказа:", + 'whatsapp_order_link': "Ссылка на заказ:", + 'whatsapp_contact_me': "Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.", + 'error': "Ошибка", + 'order_not_found': "Заказ с таким ID не найден.", + }, + 'kk': { + 'site_title': "dalarssi - Каталог", + 'brand_name': "dalarssi", + 'our_address': "Біздің мекенжайымыз:", + 'all_categories': "Барлық санаттар", + 'search_placeholder': "Аты немесе сипаттамасы бойынша іздеу...", + 'no_products_added': "Тауарлар әлі қосылмаған.", + 'no_products_found': "Сіздің сұранысыңыз бойынша тауарлар табылмады.", + 'top_product': "Топ", + 'price_per_line': "Лента бағасы", + 'price_per_piece': "Дана бағасы", + 'items_in_line': "лентада дана", + 'details': "Толығырақ", + 'add_to_cart': "Себетке қосу", + 'loading': "Жүктелуде...", + 'specify_quantity_color': "Саны мен түсін көрсетіңіз", + 'quantity': "Саны (лента):", + 'color_variant': "Түсі/нұсқасы:", + 'confirm_add_to_cart': "Себетке қосу", + 'your_cart': "Сіздің себетіңіз", + 'cart_is_empty': "Сіздің себетіңіз бос.", + 'total': "Жиыны:", + 'clear_cart': "Себетті тазалау", + 'formulate_order': "Тапсырысты рәсімдеу", + 'open_cart': "Себетті ашу", + 'close': "Жабу", + 'product_added_notification': "себетке қосылды!", + 'product_load_error': "Тауар туралы ақпаратты жүктеу мүмкін болмады.", + 'enter_correct_quantity': "Дұрыс санды көрсетіңіз (0-ден көп).", + 'product_add_error': "Қосу қатесі: тауар табылмады.", + 'confirm_clear_cart': "Себетті тазалағыңыз келетініне сенімдісіз бе?", + 'cart_empty_error': "Себет бос! Тапсырысты рәсімдеу алдында тауарларды қосыңыз.", + 'formulating_order': "Тапсырысты рәсімдеудеміз...", + 'order_creation_error': "Тапсырысты рәсімдеу кезінде қате:", + 'category': "Санат:", + 'no_category': "Санатсыз", + 'price': "Бағасы:", + 'description': "Сипаттамасы:", + 'no_description': "Сипаттамасы жоқ.", + 'available_colors': "Қолжетімді түстер/нұсқалар:", + 'order_title': "Тапсырыс №", + 'your_order': "Сіздің Тапсырысыңыз №", + 'creation_date': "Құрылған күні:", + 'products_in_order': "Тапсырыстағы тауарлар", + 'total_to_pay': "ТӨЛЕУГЕ ЖИЫНЫ:", + 'order_status': "Тапсырыс мәртебесі", + 'order_status_desc': "Бұл тапсырыс жүйеге кірмей рәсімделді. Растау және мәліметтерді нақтылау үшін WhatsApp арқылы бізбен хабарласыңыз.", + 'send_order': "Тапсырысты жіберу", + 'back_to_catalog': "← Каталогқа оралу", + 'whatsapp_greeting': "Сәлеметсіз бе! Мен dalarssi-дегі тапсырысымды растағым келеді:", + 'whatsapp_order_number': "Тапсырыс нөмірі:", + 'whatsapp_order_link': "Тапсырысқа сілтеме:", + 'whatsapp_contact_me': "Төлем және жеткізу мәліметтерін нақтылау үшін менімен хабарласыңыз.", + 'error': "Қате", + 'order_not_found': "Бұл ID-мен тапсырыс табылмады.", + } +} + + +def get_locale(): + return session.get('lang', 'ru') + +def _(key): + lang = get_locale() + return translations.get(lang, {}).get(key, key) + +@app.context_processor +def inject_helpers(): + return dict(_=_, lang=get_locale(), translations=translations) + +@app.route('/lang/') +def set_language(language): + session['lang'] = language if language in translations else 'ru' + return redirect(request.referrer or url_for('catalog')) def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): @@ -206,140 +340,113 @@ def save_data(data): CATALOG_TEMPLATE = ''' - + - dallarsi - Каталог + {{ _('site_title') }} - +
-
- -

dallarsi

+
+ +

{{ _('brand_name') }}

-
-
- | - -
- +
+ RU + KZ
-
{{ store_address }}
+
{{ _('our_address') }} {{ store_address }}
- + {% for category in categories %} {% endfor %}
- +
@@ -349,7 +456,7 @@ CATALOG_TEMPLATE = ''' data-description="{{ product.get('description', '')|lower }}" data-category="{{ product.get('category', 'Без категории') }}"> {% if product.get('is_top', False) %} - Топ + {{ _('top_product') }} {% endif %}
{% if product.get('photos') and product['photos']|length > 0 %} @@ -357,79 +464,74 @@ CATALOG_TEMPLATE = ''' alt="{{ product['name'] }}" loading="lazy"> {% else %} - No Image + No Image {% endif %}

{{ product['name'] }}

-
-
- {{ "%.2f"|format(product['price']) }} {{ currency_code }} -
- {% set pack_size = product.get('pack_size', 1) %} - {% if pack_size > 1 %} -
- {{ "%.2f"|format(product['price'] / pack_size) }} {{ currency_code }} / шт. -
- {% endif %} +
+ {{ "%.2f"|format(product['price']) }} {{ currency_code }} + + {% if product.get('items_per_line', 0) > 0 %} + {{ "%.2f"|format(product['price'] / product.get('items_per_line')) }} {{ currency_code }} / {{ _('price_per_piece') }} + ({{ product.get('items_per_line') }} {{ _('items_in_line') }}) + {% endif %} +
-

{{ product.get('description', '') }}

- +
{% endfor %} {% if not products %} -

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

+

{{ _('no_products_added') }}

{% endif %}
- @@ -441,160 +543,37 @@ CATALOG_TEMPLATE = ''' const products = {{ products|tojson }}; const repoId = '{{ repo_id }}'; const currencyCode = '{{ currency_code }}'; + const t = {{ translations[lang]|tojson|safe }}; let selectedProductIndex = null; - let cart = JSON.parse(localStorage.getItem('dallarsiCart') || '[]'); - - const translations = { - ru: { - catalog_title: "dallarsi - Каталог", - store_address_prefix: `Наш адрес: {{ store_address }}`, - all_categories: "Все категории", - search_placeholder: "Поиск по названию или описанию...", - details_button: "Подробнее", - add_to_cart_button: "В корзину", - no_products_yet: "Товары пока не добавлены.", - no_results: "По вашему запросу товары не найдены.", - loading: "Загрузка...", - quantity_modal_title: "Укажите количество и цвет", - quantity_label: "Количество линеек:", - color_label: "Цвет/Вариант:", - add_to_cart_confirm: "Добавить в корзину", - cart_title: "Ваша корзина", - cart_empty: "Ваша корзина пуста.", - cart_total: "Итого:", - cart_clear: "Очистить корзину", - cart_formulate_order: "Сформировать заказ", - notification_added: "добавлен в корзину!", - notification_formulating: "Формируем заказ...", - pack_price_prefix: "Цена за линейку:", - item_price_prefix_template: "{{price}} {{currency}} / шт. (в линейке {{pack_size}} шт.)", - cart_pack_quantity_template: "{{quantity}} x {{pack_size}} шт.", - confirm_clear_cart: "Вы уверены, что хотите очистить корзину?", - error_generic: "Ошибка", - error_not_found: "Товар не найден", - error_incorrect_quantity: "Пожалуйста, укажите корректное количество (больше 0).", - error_empty_cart: "Корзина пуста! Добавьте товары перед формированием заказа.", - error_order_fail: "Не удалось создать заказ", - error_no_order_id: "Не получен ID заказа от сервера.", - product_fetch_error: "Не удалось загрузить информацию о товаре.", - top_product_indicator: "Топ", - pack_text: "линейка", - }, - kz: { - catalog_title: "dallarsi - Каталог", - store_address_prefix: `Біздің мекенжайымыз: {{ store_address }}`, - all_categories: "Барлық санаттар", - search_placeholder: "Атауы немесе сипаттамасы бойынша іздеу...", - details_button: "Толығырақ", - add_to_cart_button: "Себетке", - no_products_yet: "Тауарлар әлі қосылмаған.", - no_results: "Сіздің сұрауыңыз бойынша тауарлар табылмады.", - loading: "Жүктелуде...", - quantity_modal_title: "Саны мен түсін көрсетіңіз", - quantity_label: "Топтама саны:", - color_label: "Түсі/Нұсқасы:", - add_to_cart_confirm: "Себетке қосу", - cart_title: "Сіздің себетіңіз", - cart_empty: "Себетіңіз бос.", - cart_total: "Жалпы:", - cart_clear: "Себетті тазалау", - cart_formulate_order: "Тапсырысты рәсімдеу", - notification_added: "себетке қосылды!", - notification_formulating: "Тапсырысты рәсімдеудеміз...", - pack_price_prefix: "Топтама бағасы:", - item_price_prefix_template: "{{price}} {{currency}} / дана (топтамада {{pack_size}} дана)", - cart_pack_quantity_template: "{{quantity}} x {{pack_size}} дана", - confirm_clear_cart: "Себетті тазалағыңыз келетініне сенімдісіз бе?", - error_generic: "Қате", - error_not_found: "Тауар табылмады", - error_incorrect_quantity: "Дұрыс санды (0-ден көп) көрсетіңіз.", - error_empty_cart: "Себет бос! Тапсырыс бермес бұрын тауар қосыңыз.", - error_order_fail: "Тапсырысты құру мүмкін болмады", - error_no_order_id: "Серверден тапсырыс ID алынбады.", - product_fetch_error: "Тауар туралы ақпаратты жүктеу мүмкін болмады.", - top_product_indicator: "Топ", - pack_text: "топтама", - } - }; - let currentLang = localStorage.getItem('dallarsiLanguage') || 'ru'; - - function applyTranslations(lang) { - if (!['ru', 'kz'].includes(lang)) lang = 'ru'; - currentLang = lang; - localStorage.setItem('dallarsiLanguage', lang); - document.documentElement.lang = lang; - - document.querySelectorAll('#lang-ru, #lang-kz').forEach(btn => btn.classList.remove('active')); - document.getElementById(`lang-${lang}`).classList.add('active'); - - const t = translations[lang]; - document.querySelectorAll('[data-translate-key]').forEach(el => { - const key = el.dataset.translateKey; - if (t[key]) { - if (key === 'item_price_prefix') { - const packSize = el.dataset.packSize; - const pricePerItem = parseFloat(el.textContent.match(/[\d.]+/)[0]); - el.textContent = t.item_price_prefix_template - .replace('{{price}}', pricePerItem.toFixed(2)) - .replace('{{currency}}', currencyCode) - .replace('{{pack_size}}', packSize); - } else { - el.innerHTML = t[key]; - } - } - }); - document.querySelectorAll('[data-translate-key-placeholder]').forEach(el => { - const key = el.dataset.translateKeyPlaceholder; - if (t[key]) { - el.placeholder = t[key]; - } - }); - document.title = t.catalog_title; - filterProducts(false); - } - - function t(key) { - return translations[currentLang][key] || translations['ru'][key] || key; - } - - - 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.toggle('fa-moon', !isDarkMode); - icon.classList.toggle('fa-sun', isDarkMode); - localStorage.setItem('dallarsiTheme', isDarkMode ? 'dark' : 'light'); - } - - function applyInitialTheme() { - if (localStorage.getItem('dallarsiTheme') === 'dark') { - document.body.classList.add('dark-mode'); - const icon = document.querySelector('.theme-toggle i'); - if (icon) icon.classList.replace('fa-moon', 'fa-sun'); - } - } + let cart = JSON.parse(localStorage.getItem('dalarssiCart') || '[]'); function openModal(index) { loadProductDetails(index); - document.getElementById('productModal').style.display = "block"; - document.body.style.overflow = 'hidden'; + const modal = document.getElementById('productModal'); + if (modal) { + modal.style.display = "block"; + document.body.style.overflow = 'hidden'; + } } function closeModal(modalId) { const modal = document.getElementById(modalId); - if (modal) modal.style.display = "none"; - if (!document.querySelector('.modal[style*="display: block"]')) { + if (modal) { + modal.style.display = "none"; + } + const anyModalOpen = document.querySelector('.modal[style*="display: block"]'); + if (!anyModalOpen) { document.body.style.overflow = 'auto'; } } function loadProductDetails(index) { const modalContent = document.getElementById('modalContent'); - modalContent.innerHTML = `

${t('loading')}

`; - fetch(`/product/${index}?lang=${currentLang}`) + if (!modalContent) return; + modalContent.innerHTML = `

${t.loading}...

`; + fetch(`/product/${index}?lang=${'{{lang}}'}`) .then(response => { - if (!response.ok) throw new Error(`${response.status}: ${response.statusText}`); + if (!response.ok) throw new Error(`Error ${response.status}: ${response.statusText}`); return response.text(); }) .then(data => { @@ -602,17 +581,20 @@ CATALOG_TEMPLATE = ''' initializeSwiper(); }) .catch(error => { - modalContent.innerHTML = `

${t('product_fetch_error')} ${error.message}

`; + console.error('Error loading product details:', error); + modalContent.innerHTML = `

${t.product_load_error} ${error.message}

`; }); } function initializeSwiper() { - if (document.querySelector('#productModal .swiper-container')) { - new Swiper('#productModal .swiper-container', { + const swiperContainer = document.querySelector('#productModal .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, containerClass: 'swiper-zoom-container' }, + autoplay: { delay: 5000, disableOnInteraction: true, }, }); } } @@ -621,7 +603,8 @@ CATALOG_TEMPLATE = ''' selectedProductIndex = index; const product = products[index]; if (!product) { - alert(`${t('error_generic')}: ${t('error_not_found')}`); + console.error("Product not found for index:", index); + alert(t.product_add_error); return; } @@ -646,51 +629,64 @@ CATALOG_TEMPLATE = ''' } document.getElementById('quantityInput').value = 1; - document.getElementById('quantityModal').style.display = 'block'; - document.body.style.overflow = 'hidden'; + const modal = document.getElementById('quantityModal'); + if(modal) { + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } } function confirmAddToCart() { if (selectedProductIndex === null) return; - const quantity = parseInt(document.getElementById('quantityInput').value); + + const quantityInput = document.getElementById('quantityInput'); + const quantity = parseInt(quantityInput.value); const colorSelect = document.getElementById('colorSelect'); const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A'; if (isNaN(quantity) || quantity <= 0) { - alert(t('error_incorrect_quantity')); + alert(t.enter_correct_quantity); + quantityInput.focus(); return; } + const product = products[selectedProductIndex]; - if (!product) { - alert(`${t('error_generic')}: ${t('error_not_found')}`); + if (!product) { + alert(t.product_add_error); return; - } + } + const cartItemId = `${product.name}-${color}`; - const existingItem = cart.find(item => item.id === cartItemId); - if (existingItem) { - existingItem.quantity += quantity; + const existingItemIndex = cart.findIndex(item => item.id === cartItemId); + + if (existingItemIndex > -1) { + cart[existingItemIndex].quantity += quantity; } else { cart.push({ id: cartItemId, name: product.name, price: product.price, photo: product.photos && product.photos.length > 0 ? product.photos[0] : null, - quantity: quantity, color: color, pack_size: product.pack_size || 1 + quantity: quantity, color: color, items_per_line: product.items_per_line }); } - localStorage.setItem('dallarsiCart', JSON.stringify(cart)); + + localStorage.setItem('dalarssiCart', JSON.stringify(cart)); closeModal('quantityModal'); updateCartButton(); - showNotification(`${product.name} ${t('notification_added')}`); + showNotification(`${product.name} ${t.product_added_notification}`); } function updateCartButton() { const cartCountElement = document.getElementById('cart-count'); const cartButton = document.getElementById('cart-button'); if (!cartCountElement || !cartButton) return; - let totalItems = cart.reduce((sum, item) => sum + item.quantity, 0); + let totalItems = 0; + cart.forEach(item => { totalItems += item.quantity; }); + if (totalItems > 0) { cartCountElement.textContent = totalItems; cartButton.style.display = 'flex'; } else { + cartCountElement.textContent = '0'; cartButton.style.display = 'none'; } } @@ -698,45 +694,53 @@ CATALOG_TEMPLATE = ''' function openCartModal() { const cartContent = document.getElementById('cartContent'); const cartTotalElement = document.getElementById('cartTotal'); + if (!cartContent || !cartTotalElement) return; let total = 0; + if (cart.length === 0) { - cartContent.innerHTML = `

${t('cart_empty')}

`; + cartContent.innerHTML = `

${t.cart_is_empty}

`; cartTotalElement.textContent = '0.00'; } else { cartContent.innerHTML = cart.map(item => { const itemTotal = item.price * item.quantity; total += itemTotal; - const photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60.png?text=dallarsi'; + const photoUrl = item.photo + ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` + : 'https://via.placeholder.com/70x70.png?text=N/A'; const colorText = item.color !== 'N/A' ? ` (${item.color})` : ''; - const packInfo = t('cart_pack_quantity_template').replace('{{quantity}}', item.quantity).replace('{{pack_size}}', item.pack_size); + const lineInfo = item.items_per_line ? ` (${item.items_per_line} ${t.items_in_line})` : ''; + return `
${item.name}
${item.name}${colorText} -

${packInfo}

+

${item.quantity} × ${item.price.toFixed(2)} ${currencyCode}${lineInfo}

${itemTotal.toFixed(2)} ${currencyCode} - +
`; }).join(''); cartTotalElement.textContent = total.toFixed(2); } - document.getElementById('cartModal').style.display = 'block'; - document.body.style.overflow = 'hidden'; + const modal = document.getElementById('cartModal'); + if (modal) { + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } } function removeFromCart(itemId) { cart = cart.filter(item => item.id !== itemId); - localStorage.setItem('dallarsiCart', JSON.stringify(cart)); + localStorage.setItem('dalarssiCart', JSON.stringify(cart)); openCartModal(); updateCartButton(); } function clearCart() { - if (confirm(t('confirm_clear_cart'))) { + if (confirm(t.confirm_clear_cart)) { cart = []; - localStorage.removeItem('dallarsiCart'); + localStorage.removeItem('dalarssiCart'); openCartModal(); updateCartButton(); } @@ -744,50 +748,60 @@ CATALOG_TEMPLATE = ''' function formulateOrder() { if (cart.length === 0) { - alert(t('error_empty_cart')); + alert(t.cart_empty_error); return; } + const orderData = { cart: cart }; const formulateButton = document.querySelector('.formulate-order-button'); if (formulateButton) formulateButton.disabled = true; - showNotification(t('notification_formulating'), 5000); + showNotification(t.formulating_order, 5000); fetch('/create_order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cart: cart }) + body: JSON.stringify(orderData) }) .then(response => { - if (!response.ok) return response.json().then(err => { throw new Error(err.error || t('error_order_fail')); }); + if (!response.ok) { + return response.json().then(err => { throw new Error(err.error || 'Failed to create order'); }); + } return response.json(); }) .then(data => { if (data.order_id) { - localStorage.removeItem('dallarsiCart'); + localStorage.removeItem('dalarssiCart'); cart = []; updateCartButton(); closeModal('cartModal'); window.location.href = `/order/${data.order_id}`; } else { - throw new Error(t('error_no_order_id')); + throw new Error('Order ID not received from server.'); } }) .catch(error => { - alert(`${t('error_generic')}: ${error.message}`); + console.error('Order formulation error:', error); + alert(`${t.order_creation_error} ${error.message}`); if (formulateButton) formulateButton.disabled = false; }); } - function filterProducts(applyLang = true) { + + function filterProducts() { const searchTerm = document.getElementById('search-input').value.toLowerCase().trim(); - const activeCategory = document.querySelector('.category-filter.active')?.dataset.category || 'all'; + const activeCategoryButton = document.querySelector('.category-filter.active'); + const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all'; const grid = document.getElementById('products-grid'); let visibleProducts = 0; - grid.querySelector('.no-results-message')?.remove(); + + const existingNoResults = grid.querySelector('.no-results-message'); + if (existingNoResults) existingNoResults.remove(); + document.querySelectorAll('.products-grid .product').forEach(productElement => { - const name = productElement.dataset.name; - const description = productElement.dataset.description; - const category = productElement.dataset.category; + const name = productElement.getAttribute('data-name'); + const description = productElement.getAttribute('data-description'); + const category = productElement.getAttribute('data-category'); const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm); const matchesCategory = activeCategory === 'all' || category === activeCategory; + if (matchesSearch && matchesCategory) { productElement.style.display = 'flex'; visibleProducts++; @@ -796,46 +810,36 @@ CATALOG_TEMPLATE = ''' } }); - if (visibleProducts === 0 && products.length > 0) { + if (visibleProducts === 0 && products.length > 0) { const p = document.createElement('p'); p.className = 'no-results-message'; - p.dataset.translateKey = 'no_results'; - p.textContent = t('no_results'); + p.textContent = t.no_products_found; grid.appendChild(p); - } else if (products.length === 0 && !grid.querySelector('.no-results-message')) { + } else if (products.length === 0 && !grid.querySelector('.no-results-message')) { const p = document.createElement('p'); p.className = 'no-results-message'; - p.dataset.translateKey = 'no_products_yet'; - p.textContent = t('no_products_yet'); + p.textContent = t.no_products_added; grid.appendChild(p); - } - - if (applyLang) { - document.querySelectorAll('[data-translate-key="item_price_prefix"]').forEach(el => { - const packSize = el.dataset.packSize; - const pricePerItem = parseFloat(el.textContent.match(/[\d.]+/)[0]); - el.textContent = t('item_price_prefix_template') - .replace('{{price}}', pricePerItem.toFixed(2)) - .replace('{{currency}}', currencyCode) - .replace('{{pack_size}}', packSize); - }); - } + } } function setupFilters() { - document.getElementById('search-input')?.addEventListener('input', () => filterProducts(false)); - document.querySelectorAll('.category-filter').forEach(filter => { + const searchInput = document.getElementById('search-input'); + const categoryFilters = document.querySelectorAll('.category-filter'); + if(searchInput) searchInput.addEventListener('input', filterProducts); + categoryFilters.forEach(filter => { filter.addEventListener('click', function() { - document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active')); + categoryFilters.forEach(f => f.classList.remove('active')); this.classList.add('active'); - filterProducts(false); + filterProducts(); }); }); - filterProducts(true); + filterProducts(); } function showNotification(message, duration = 3000) { const placeholder = document.getElementById('notification-placeholder'); + if (!placeholder) return; const notification = document.createElement('div'); notification.className = 'notification'; notification.textContent = message; @@ -849,16 +853,21 @@ CATALOG_TEMPLATE = ''' } document.addEventListener('DOMContentLoaded', () => { - applyInitialTheme(); updateCartButton(); setupFilters(); - applyTranslations(currentLang); - window.addEventListener('click', e => { if (e.target.classList.contains('modal')) closeModal(e.target.id); }); - window.addEventListener('keydown', e => { - if (e.key === 'Escape') document.querySelectorAll('.modal').forEach(m => closeModal(m.id)); + window.addEventListener('click', function(event) { + if (event.target.classList.contains('modal')) { + closeModal(event.target.id); + } + }); + window.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => { + closeModal(modal.id); + }); + } }); }); - @@ -866,7 +875,7 @@ CATALOG_TEMPLATE = ''' PRODUCT_DETAIL_TEMPLATE = '''
-

{{ product['name'] }}

+

{{ product['name'] }}

{% if product.get('photos') and product['photos']|length > 0 %} @@ -874,42 +883,38 @@ PRODUCT_DETAIL_TEMPLATE = '''
{{ product['name'] }} - фото {{ loop.index }} + alt="{{ product['name'] }} - photo {{ loop.index }}" + style="max-width: 100%; max-height: 400px; object-fit: contain; display: block; margin: auto; cursor: grab;">
{% endfor %} {% else %}
- Изображение отсутствует + No image available
{% endif %}
{% if product.get('photos') and product['photos']|length > 1 %}
-
-
+
+
{% endif %}
-
-

{{ translations[lang]['category'] }}: {{ product.get('category', translations[lang]['no_category']) }}

- - {% set pack_size = product.get('pack_size', 1) %} -

- {{ translations[lang]['pack_price'] }}: {{ "%.2f"|format(product['price']) }} {{ currency_code }} +

+

{{ _('category') }} {{ product.get('category', _('no_category')) }}

+

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

- {% if pack_size > 1 %} -

- ({{ "%.2f"|format(product['price'] / pack_size) }} {{ currency_code }} {{ translations[lang]['per_item'] }}) + {% if product.get('items_per_line', 0) > 0 %} +

+ {{ "%.2f"|format(product['price'] / product.get('items_per_line')) }} {{ currency_code }} / {{ _('price_per_piece') }} ({{ product.get('items_per_line') }} {{ _('items_in_line') }})

{% endif %} - -

{{ translations[lang]['description'] }}:
{{ product.get('description', translations[lang]['no_description'])|replace('\\n', '
')|safe }}

- +

{{ _('description') }}
{{ product.get('description', _('no_description'))|replace('\\n', '
')|safe }}

{% set colors = product.get('colors', []) %} {% if colors and colors|select('ne', '')|list|length > 0 %} -

{{ translations[lang]['available_colors'] }}: {{ colors|select('ne', '')|join(', ') }}

+

{{ _('available_colors') }} {{ colors|select('ne', '')|join(', ') }}

{% endif %}
@@ -917,58 +922,62 @@ PRODUCT_DETAIL_TEMPLATE = ''' ORDER_TEMPLATE = ''' - + - {{ translations[lang]['order_title'] }} №{{ order.id }} - dallarsi - + {{ _('order_title') }}{{ order.id }} - dalarssi +
{% if order %} -

{{ translations[lang]['your_order'] }} №{{ order.id }}

-

{{ translations[lang]['created_at'] }}: {{ order.created_at }}

+

{{ _('your_order') }}{{ order.id }}

+

{{ _('creation_date') }}: {{ order.created_at }}

-

{{ translations[lang]['items_in_order'] }}

+

{{ _('products_in_order') }}

{% for item in order.cart %}
{{ item.name }}
{{ item.name }} {% if item.color != 'N/A' %}({{ item.color }}){% endif %} - {% set pack_size = item.get('pack_size', 1) %} - {{ item.quantity }} x {{ "%.2f"|format(item.price) }} {{ currency_code }} ({{ translations[lang]['pack_text'] }}) - {% if pack_size > 1 %} - {{ translations[lang]['total_items_in_order'] }}: {{ item.quantity * pack_size }} {{ translations[lang]['pcs'] }} - {% endif %} + {{ item.quantity }} × {{ "%.2f"|format(item.price) }} {{ currency_code }}
{{ "%.2f"|format(item.price * item.quantity) }} {{ currency_code }} @@ -978,49 +987,30 @@ ORDER_TEMPLATE = '''
-

{{ translations[lang]['total_price_items'] }}: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}

-

{{ translations[lang]['total_to_pay'] }}: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}

+

{{ _('total_to_pay') }}: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}

-

{{ translations[lang]['order_status'] }}

-

{{ translations[lang]['anon_order_notice'] }}

-

{{ translations[lang]['contact_for_details'] }}

+

{{ _('order_status') }}

+

{{ _('order_status_desc') }}

- +
- ← {{ translations[lang]['back_to_catalog'] }} + {{ _('back_to_catalog') }} {% else %} -

{{ translations[lang]['error_title'] }}

-

{{ translations[lang]['order_not_found'] }}

- ← {{ translations[lang]['back_to_catalog'] }} +

{{ _('error') }}

+

{{ _('order_not_found') }}

+ {{ _('back_to_catalog') }} {% endif %}
@@ -1043,45 +1033,60 @@ ADMIN_TEMPLATE = ''' - Админ-панель - dallarsi - + Админ-панель - dalarssi + @@ -1096,12 +1103,13 @@ ADMIN_TEMPLATE = '''
- -

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

+ +

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

- Перейти в каталог + Перейти в каталог
+ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} @@ -1114,12 +1122,13 @@ ADMIN_TEMPLATE = '''

Синхронизация с Датацентром

- +
- +
+

Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.

@@ -1133,17 +1142,18 @@ ADMIN_TEMPLATE = ''' - +
+

Существующие категории:

{% if categories %}
{% for category in categories %}
{{ category }} -
+ @@ -1156,6 +1166,7 @@ ADMIN_TEMPLATE = ''' {% endif %}
+

Информация

@@ -1176,24 +1187,38 @@ ADMIN_TEMPLATE = ''' - - + + - -
- + +
+
+ + +
+
+
-
+
+ + +
+
+ + +

- +
@@ -1206,21 +1231,37 @@ ADMIN_TEMPLATE = '''
{% if product.get('photos') %} - Фото + + Фото + {% else %} - Нет фото + Нет фото {% endif %}
-

+

{{ product['name'] }} - {% if product.get('is_top', False) %} Топ{% endif %} + {% if product.get('in_stock', True) %} + В наличии + {% else %} + Нет в наличии + {% endif %} + {% if product.get('is_top', False) %} + Топ + {% endif %}

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

-

Цена за линейку ({{ product.get('pack_size', 1) }} шт.): {{ "%.2f"|format(product['price']) }} {{ currency_code }}

-

Цвета/Вар-ты: {{ product.get('colors', [])|select('ne', '')|join(', ') if product.get('colors', [])|select('ne', '')|list|length > 0 else 'Нет' }}

+

Цена за линейку: {{ "%.2f"|format(product['price']) }} {{ currency_code }}

+

Шт. в линейке: {{ product.get('items_per_line', 'N/A') }}

+

Описание: {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}

+ {% set colors = product.get('colors', []) %} +

Цвета/Вар-ты: {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}

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

(Всего фото: {{ product['photos']|length }})

+ {% endif %}
+
@@ -1229,37 +1270,68 @@ ADMIN_TEMPLATE = '''
+
+

Редактирование: {{ product['name'] }}

- - - - + + + + + + + + - + + {% if product.get('photos') %} -
{% for photo in product['photos'] %}{% endfor %}
+

Текущие фото:

+
+ {% for photo in product['photos'] %} + Фото {{ loop.index }} + {% endfor %} +
{% endif %}
{% set current_colors = product.get('colors', []) %} {% if current_colors and current_colors|select('ne', '')|list|length > 0 %} - {% for color in current_colors %}{% if color.strip() %}
{% endif %}{% endfor %} + {% for color in current_colors %} + {% if color.strip() %} +
+ + +
+ {% endif %} + {% endfor %} {% else %} -
+
+ + +
{% endif %}
- +
-
+
+ + +
+
+ + +

- +
@@ -1269,24 +1341,47 @@ ADMIN_TEMPLATE = '''

Товаров пока нет.

{% endif %}
+
+ @@ -1300,7 +1395,10 @@ def catalog(): data = load_data() all_products = data.get('products', []) categories = sorted(data.get('categories', [])) - products_sorted = sorted(all_products, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) + + products_in_stock = [p for p in all_products if p.get('in_stock', True)] + products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) + return render_template_string( CATALOG_TEMPLATE, products=products_sorted, @@ -1312,80 +1410,85 @@ def catalog(): @app.route('/product/') def product_detail(index): + # Set language from query param if provided, for fetch requests + lang = request.args.get('lang', 'ru') + session['lang'] = lang if lang in translations else 'ru' + data = load_data() all_products = data.get('products', []) - products_sorted = sorted(all_products, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) - - translations = { - 'ru': { - 'category': "Категория", 'no_category': "Без категории", 'pack_price': "Цена за линейку", - 'per_item': "за шт.", 'description': "Описание", 'no_description': "Описание отсутствует.", - 'available_colors': "Доступные цвета/варианты" - }, - 'kz': { - 'category': "Санат", 'no_category': "Санатсыз", 'pack_price': "Топтама бағасы", - 'per_item': "дана үшін", 'description': "Сипаттама", 'no_description': "Сипаттама жоқ.", - 'available_colors': "Қолжетімді түстер/нұсқалар" - } - } - lang = request.args.get('lang', 'ru') - if lang not in translations: - lang = 'ru' + products_in_stock = [p for p in all_products if p.get('in_stock', True)] + products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) try: product = products_sorted[index] except IndexError: - return "Товар не найден", 404 + logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}") + return _("product_not_found"), 404 return render_template_string( PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID, - currency_code=CURRENCY_CODE, - translations=translations, - lang=lang + currency_code=CURRENCY_CODE ) @app.route('/create_order', methods=['POST']) def create_order(): order_data = request.get_json() + if not order_data or 'cart' not in order_data or not order_data['cart']: - return jsonify({"error": "Корзина пуста."}), 400 + logging.warning("Create order request missing cart data or cart is empty.") + return jsonify({"error": "Корзина пуста или не передана."}), 400 cart_items = order_data['cart'] + total_price = 0 processed_cart = [] for item in cart_items: + if not all(k in item for k in ('name', 'price', 'quantity')): + logging.error(f"Invalid cart item structure received: {item}") + return jsonify({"error": "Неверный формат товара в корзине."}), 400 try: price = float(item['price']) quantity = int(item['quantity']) - pack_size = int(item.get('pack_size', 1)) - if price < 0 or quantity <= 0 or pack_size < 1: + if price < 0 or quantity <= 0: raise ValueError("Invalid price or quantity") total_price += price * quantity processed_cart.append({ - "name": item['name'], "price": price, "quantity": quantity, - "color": item.get('color', 'N/A'), "photo": item.get('photo'), - "pack_size": pack_size, - "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=dallarsi" + "name": item['name'], + "price": price, + "quantity": quantity, + "color": item.get('color', 'N/A'), + "photo": item.get('photo'), + "items_per_line": item.get('items_per_line'), + "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A" }) - except (ValueError, TypeError, KeyError) as e: - logging.error(f"Invalid cart item: {item}. Error: {e}") - return jsonify({"error": "Неверный формат товара в корзине."}), 400 + except (ValueError, TypeError) as e: + logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}") + return jsonify({"error": "Неверная цена или количество в товаре."}), 400 order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}" + order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + new_order = { - "id": order_id, "created_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - "cart": processed_cart, "total_price": round(total_price, 2), - "user_info": None, "status": "new" + "id": order_id, + "created_at": order_timestamp, + "cart": processed_cart, + "total_price": round(total_price, 2), + "user_info": None, + "status": "new" } + try: data = load_data() - data['orders'] = data.get('orders', {}) + if 'orders' not in data or not isinstance(data.get('orders'), dict): + data['orders'] = {} + data['orders'][order_id] = new_order save_data(data) - logging.info(f"Order {order_id} created.") + logging.info(f"Order {order_id} created successfully (anonymously).") return jsonify({"order_id": order_id}), 201 + except Exception as e: logging.error(f"Failed to save order {order_id}: {e}", exc_info=True) return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500 @@ -1394,27 +1497,16 @@ def create_order(): def view_order(order_id): data = load_data() order = data.get('orders', {}).get(order_id) - translations = { - 'ru': { - 'order_title': "Заказ", 'your_order': "Ваш Заказ", 'created_at': "Дата создания", - 'items_in_order': "Товары в заказе", 'pack_text': "линейка", 'total_items_in_order': "Всего", 'pcs': "шт.", - 'total_price_items': "Общая сумма товаров", 'total_to_pay': "ИТОГО К ОПЛАТЕ", - 'order_status': "Статус заказа", 'anon_order_notice': "Этот заказ был оформлен без входа в систему.", - 'contact_for_details': "Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.", - 'send_order': "Отправить заказ", 'back_to_catalog': "Вернуться в каталог", - 'error_title': "Ошибка", 'order_not_found': "Заказ с таким ID не найден." - }, - 'kz': { - 'order_title': "Тапсырыс", 'your_order': "Сіздің Тапсырысыңыз", 'created_at': "Құрылған күні", - 'items_in_order': "Тапсырыстағы тауарлар", 'pack_text': "топтама", 'total_items_in_order': "Барлығы", 'pcs': "дана", - 'total_price_items': "Тауарлардың жалпы сомасы", 'total_to_pay': "ТӨЛЕУГЕ ЖАТАТЫН ЖАЛПЫ СОМАСЫ", - 'order_status': "Тапсырыс мәртебесі", 'anon_order_notice': "Бұл тапсырыс жүйеге кірмей рәсімделді.", - 'contact_for_details': "Растау және мәліметтерді нақтылау үшін WhatsApp арқылы бізбен хабарласыңыз.", - 'send_order': "Тапсырысты жіберу", 'back_to_catalog': "Каталогқа оралу", - 'error_title': "Қате", 'order_not_found': "Мұндай ID-мен тапсырыс табылмады." - } - } - return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID, currency_code=CURRENCY_CODE, translations=translations, lang='ru') + + if order: + logging.info(f"Displaying order {order_id}") + else: + logging.warning(f"Order {order_id} not found.") + + return render_template_string(ORDER_TEMPLATE, + order=order, + repo_id=REPO_ID, + currency_code=CURRENCY_CODE) @app.route('/admin', methods=['GET', 'POST']) def admin(): @@ -1428,10 +1520,18 @@ def admin(): product['id'] = str(uuid.uuid4()) needs_save = True if needs_save: + logging.info("Assigning unique IDs to products that were missing them.") + data['products'] = products save_data(data) + + if 'orders' not in data or not isinstance(data.get('orders'), dict): + data['orders'] = {} + if request.method == 'POST': action = request.form.get('action') + logging.info(f"Admin action received: {action}") + try: if action == 'add_category': category_name = request.form.get('category_name', '').strip() @@ -1439,146 +1539,306 @@ def admin(): categories.append(category_name) data['categories'] = categories save_data(data) + logging.info(f"Category '{category_name}' added.") flash(f"Категория '{category_name}' успешно добавлена.", 'success') + elif not category_name: + logging.warning("Attempted to add empty category.") + flash("Название категории не может быть пустым.", 'error') else: - flash(f"Категория '{category_name}' уже существует или пуста.", 'error') + logging.warning(f"Category '{category_name}' already exists.") + flash(f"Категория '{category_name}' уже существует.", 'error') elif action == 'delete_category': category_to_delete = request.form.get('category_name') - if category_to_delete in categories: + if category_to_delete and category_to_delete in categories: categories.remove(category_to_delete) + updated_count = 0 for product in products: if product.get('category') == category_to_delete: product['category'] = 'Без категории' + updated_count += 1 data['categories'] = categories data['products'] = products save_data(data) - flash(f"Категория '{category_to_delete}' удалена.", 'success') + logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.") + flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success') else: - flash(f"Не удалось удалить категорию.", 'error') + logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}") + flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') elif action == 'add_product': name = request.form.get('name', '').strip() price_str = request.form.get('price', '').replace(',', '.') - pack_size_str = request.form.get('pack_size', '1').strip() - if not name or not price_str: - flash("Название и цена товара обязательны.", 'error') + items_per_line_str = request.form.get('items_per_line', '1') + description = request.form.get('description', '').strip() + category = request.form.get('category') + photos_files = request.files.getlist('photos') + colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] + in_stock = 'in_stock' in request.form + is_top = 'is_top' in request.form + + if not name or not price_str or not items_per_line_str: + flash("Название, цена и кол-во в линейке обязательны.", 'error') return redirect(url_for('admin')) - price = round(float(price_str), 2) - pack_size = int(pack_size_str) if pack_size_str.isdigit() and int(pack_size_str) > 0 else 1 - + + try: + price = round(float(price_str), 2) + items_per_line = int(items_per_line_str) + if price < 0 or items_per_line <= 0: + raise ValueError + except ValueError: + flash("Неверный формат цены или кол-ва в линейке.", 'error') + return redirect(url_for('admin')) + photos_list = [] - photos_files = request.files.getlist('photos') if photos_files and HF_TOKEN_WRITE: + uploads_dir = 'uploads_temp' + os.makedirs(uploads_dir, exist_ok=True) api = HfApi() - for photo in photos_files[:10]: + photo_limit = 10 + uploaded_count = 0 + for photo in photos_files: + if uploaded_count >= photo_limit: + logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.") + flash(f"Загружено только первые {photo_limit} фото.", "warning") + break if photo and photo.filename: try: - safe_name = secure_filename(name)[:50] - photo_filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{os.path.splitext(photo.filename)[1]}" - api.upload_file(path_or_fileobj=photo, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + ext = os.path.splitext(photo.filename)[1].lower() + if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: + logging.warning(f"Skipping non-image file upload: {photo.filename}") + flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning") + continue + + safe_name = secure_filename(name.replace(' ', '_'))[:50] + photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}" + temp_path = os.path.join(uploads_dir, photo_filename) + photo.save(temp_path) + logging.info(f"Uploading photo {photo_filename} to HF for product {name}...") + 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"Add photo for product {name}" + ) photos_list.append(photo_filename) + logging.info(f"Photo {photo_filename} uploaded successfully.") + os.remove(temp_path) + uploaded_count += 1 except Exception as e: - flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error') + logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True) + flash(f"Ошибка при загрузке фото {photo.filename}.", 'error') + if os.path.exists(temp_path): + try: os.remove(temp_path) + except OSError: pass + elif photo and not photo.filename: + logging.warning("Received an empty photo file object when adding product.") + try: + if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): + os.rmdir(uploads_dir) + except OSError as e: + logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}") + elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files): + flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning") + new_product = { - 'id': str(uuid.uuid4()), 'name': name, 'price': price, 'pack_size': pack_size, - 'description': request.form.get('description', '').strip(), - 'category': request.form.get('category', 'Без категории'), - 'photos': photos_list, - 'colors': [c.strip() for c in request.form.getlist('colors') if c.strip()], - 'is_top': 'is_top' in request.form + 'id': str(uuid.uuid4()), + 'name': name, 'price': price, 'description': description, + 'category': category if category in categories else 'Без категории', + 'photos': photos_list, 'colors': colors, + 'in_stock': in_stock, 'is_top': is_top, + 'items_per_line': items_per_line } products.append(new_product) data['products'] = products save_data(data) + logging.info(f"Product '{name}' added.") flash(f"Товар '{name}' успешно добавлен.", 'success') elif action == 'edit_product': product_id = request.form.get('product_id') - product_to_edit = next((p for p in products if p.get('id') == product_id), None) - if not product_to_edit: - flash(f"Ошибка: товар с ID '{product_id}' не найден.", 'error') + if not product_id: + flash("Ошибка редактирования: ID товара не передан.", 'error') + return redirect(url_for('admin')) + + product_index = next((i for i, p in enumerate(products) if p.get('id') == product_id), -1) + + if product_index == -1: + flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error') + logging.error(f"Invalid product ID '{product_id}' for editing.") return redirect(url_for('admin')) + product_to_edit = products[product_index] + original_name = product_to_edit.get('name', 'N/A') + product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip() price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.') - pack_size_str = request.form.get('pack_size', str(product_to_edit.get('pack_size', 1))).strip() - product_to_edit['price'] = round(float(price_str), 2) - product_to_edit['pack_size'] = int(pack_size_str) if pack_size_str.isdigit() and int(pack_size_str) > 0 else 1 + items_per_line_str = request.form.get('items_per_line', str(product_to_edit.get('items_per_line', '1'))) product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip() - product_to_edit['category'] = request.form.get('category', 'Без категории') + category = request.form.get('category') + product_to_edit['category'] = category if category in categories else 'Без категории' product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()] + product_to_edit['in_stock'] = 'in_stock' in request.form product_to_edit['is_top'] = 'is_top' in request.form + try: + price = round(float(price_str), 2) + items_per_line = int(items_per_line_str) + if price < 0 or items_per_line <= 0: raise ValueError + product_to_edit['price'] = price + product_to_edit['items_per_line'] = items_per_line + except ValueError: + logging.warning(f"Invalid price/line format during edit of product {original_name}. Not changed.") + flash(f"Неверный формат цены/кол-ва для товара '{original_name}'. Не изменено.", 'warning') + photos_files = request.files.getlist('photos') if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE: + uploads_dir = 'uploads_temp' + os.makedirs(uploads_dir, exist_ok=True) api = HfApi() - if product_to_edit.get('photos'): - api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_to_edit['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) - - new_photos = [] - for photo in photos_files[:10]: - if photo and photo.filename: - safe_name = secure_filename(product_to_edit['name'])[:50] - photo_filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{os.path.splitext(photo.filename)[1]}" - api.upload_file(path_or_fileobj=photo, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - new_photos.append(photo_filename) - product_to_edit['photos'] = new_photos + new_photos_list = [] + photo_limit = 10 + uploaded_count = 0 + logging.info(f"Uploading new photos for product {product_to_edit['name']}...") + for photo in photos_files: + if uploaded_count >= photo_limit: + break + if photo and photo.filename: + try: + ext = os.path.splitext(photo.filename)[1].lower() + if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue + + safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50] + photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}" + temp_path = os.path.join(uploads_dir, photo_filename) + 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"Update photo for product {product_to_edit['name']}") + new_photos_list.append(photo_filename) + os.remove(temp_path) + uploaded_count += 1 + except Exception as e: + logging.error(f"Error uploading new photo {photo.filename}: {e}", exc_info=True) + if os.path.exists(temp_path): + try: os.remove(temp_path) + except OSError: pass + try: + if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): + os.rmdir(uploads_dir) + except OSError as e: + logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}") + + if new_photos_list: + old_photos = product_to_edit.get('photos', []) + if old_photos: + try: + api.delete_files( + repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in old_photos], + repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"Delete old photos for product {product_to_edit['name']}" + ) + except Exception as e: + logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True) + product_to_edit['photos'] = new_photos_list + flash("Фотографии товара успешно обновлены.", "success") + products[product_index] = product_to_edit data['products'] = products save_data(data) + logging.info(f"Product '{original_name}' (ID {product_id}) updated to '{product_to_edit['name']}'.") flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success') elif action == 'delete_product': product_id = request.form.get('product_id') product_to_delete = next((p for p in products if p.get('id') == product_id), None) + if product_to_delete: - if product_to_delete.get('photos') and HF_TOKEN_WRITE: - api = HfApi() - api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_to_delete['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) - + product_name = product_to_delete.get('name', 'N/A') products.remove(product_to_delete) + + photos_to_delete = product_to_delete.get('photos', []) + if photos_to_delete and HF_TOKEN_WRITE: + try: + api = HfApi() + api.delete_files( + repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], + repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"Delete photos for deleted product {product_name}" + ) + except Exception as e: + logging.error(f"Error deleting photos {photos_to_delete} from HF: {e}", exc_info=True) + data['products'] = products save_data(data) - flash(f"Товар '{product_to_delete['name']}' удален.", 'success') + logging.info(f"Product '{product_name}' (ID {product_id}) deleted.") + flash(f"Товар '{product_name}' удален.", 'success') else: - flash(f"Ошибка: неверный ID товара '{product_id}'.", 'error') + flash(f"Ошибка удаления: неверный ID товара '{product_id}'.", 'error') + + else: + flash(f"Неизвестное действие: {action}", 'warning') return redirect(url_for('admin')) except Exception as e: - flash(f"Произошла ошибка: {e}", 'error') + logging.error(f"Error processing admin action '{action}': {e}", exc_info=True) + flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error') return redirect(url_for('admin')) - display_products = sorted(data.get('products', []), key=lambda p: p.get('name', '').lower()) - display_categories = sorted(data.get('categories', [])) - return render_template_string(ADMIN_TEMPLATE, products=display_products, categories=display_categories, repo_id=REPO_ID, currency_code=CURRENCY_CODE) + + current_data = load_data() + display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower()) + display_categories = sorted(current_data.get('categories', [])) + + return render_template_string( + ADMIN_TEMPLATE, + products=display_products, + categories=display_categories, + repo_id=REPO_ID, + currency_code=CURRENCY_CODE + ) @app.route('/force_upload', methods=['POST']) def force_upload(): + logging.info("Forcing upload to Hugging Face...") try: upload_db_to_hf() flash("Данные успешно загружены на Hugging Face.", 'success') except Exception as e: - flash(f"Ошибка при загрузке: {e}", 'error') + logging.error(f"Error during forced upload: {e}", exc_info=True) + flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error') return redirect(url_for('admin')) @app.route('/force_download', methods=['POST']) def force_download(): + logging.info("Forcing download from Hugging Face...") try: if download_db_from_hf(): - flash("Данные успешно скачаны с Hugging Face.", 'success') + flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success') + load_data() else: - flash("Не удалось скачать данные с Hugging Face.", 'error') + flash("Не удалось скачать данные с Hugging Face после нескольких попыток.", 'error') except Exception as e: - flash(f"Ошибка при скачивании: {e}", 'error') + logging.error(f"Error during forced download: {e}", exc_info=True) + flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error') return redirect(url_for('admin')) + if __name__ == '__main__': + logging.info("Application starting up. Performing initial data load/download...") download_db_from_hf() + load_data() + logging.info("Initial data load complete.") + if HF_TOKEN_WRITE: - threading.Thread(target=periodic_backup, daemon=True).start() + backup_thread = threading.Thread(target=periodic_backup, daemon=True) + backup_thread.start() + logging.info("Periodic backup thread started.") + else: + logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).") + port = int(os.environ.get('PORT', 7860)) - app.run(debug=False, host='0.0.0.0', port=port) \ No newline at end of file + logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}") + app.run(debug=False, host='0.0.0.0', port=port)