diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -222,104 +222,125 @@ CATALOG_TEMPLATE = ''' Мобильный мир - Каталог - +
-
- Мобильный мир Logo +
+ Мобильный мир Logo

Мобильный мир

{% for category in categories %} @@ -336,10 +361,6 @@ CATALOG_TEMPLATE = ''' {% endfor %}
-
- -
-
{% for product in products %}
Топ {% endif %} -
+
{% if product.get('photos') and product['photos']|length > 0 %} {{ product['name'] }}{{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}

- @@ -387,11 +407,16 @@ CATALOG_TEMPLATE = ''' @@ -441,10 +466,15 @@ CATALOG_TEMPLATE = ''' function applyInitialTheme() { const savedTheme = localStorage.getItem('soolaTheme'); - if (savedTheme === 'dark') { + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { document.body.classList.add('dark-mode'); const icon = document.querySelector('.theme-toggle i'); if (icon) icon.classList.replace('fa-moon', 'fa-sun'); + } else { + const icon = document.querySelector('.theme-toggle i'); + if (icon) icon.classList.replace('fa-sun', 'fa-moon'); } } @@ -503,6 +533,29 @@ CATALOG_TEMPLATE = ''' } } + function setupVariantSelect(selectId, labelId, variants) { + const select = document.getElementById(selectId); + const label = document.getElementById(labelId); + const validVariants = variants ? variants.filter(v => v && v.trim() !== "") : []; + + select.innerHTML = ''; + + if (validVariants.length > 0) { + validVariants.forEach(variant => { + const option = document.createElement('option'); + option.value = variant.trim(); + option.text = variant.trim(); + select.appendChild(option); + }); + select.style.display = 'block'; + if(label) label.style.display = 'block'; + } else { + select.style.display = 'none'; + if(label) label.style.display = 'none'; + } + } + + function openQuantityModal(index) { selectedProductIndex = index; const product = products[index]; @@ -512,25 +565,8 @@ CATALOG_TEMPLATE = ''' return; } - const colorSelect = document.getElementById('colorSelect'); - const colorLabel = document.querySelector('label[for="colorSelect"]'); - colorSelect.innerHTML = ''; - - const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : []; - - if (validColors.length > 0) { - validColors.forEach(color => { - const option = document.createElement('option'); - option.value = color.trim(); - option.text = color.trim(); - colorSelect.appendChild(option); - }); - colorSelect.style.display = 'block'; - if(colorLabel) colorLabel.style.display = 'block'; - } else { - colorSelect.style.display = 'none'; - if(colorLabel) colorLabel.style.display = 'none'; - } + setupVariantSelect('colorSelect', 'colorLabel', product.colors); + setupVariantSelect('modelSelect', 'modelLabel', product.models); // NEW MODEL SETUP document.getElementById('quantityInput').value = 1; const modal = document.getElementById('quantityModal'); @@ -546,7 +582,10 @@ CATALOG_TEMPLATE = ''' const quantityInput = document.getElementById('quantityInput'); const quantity = parseInt(quantityInput.value); const colorSelect = document.getElementById('colorSelect'); + const modelSelect = document.getElementById('modelSelect'); + const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A'; + const model = modelSelect.style.display !== 'none' && modelSelect.value ? modelSelect.value : 'N/A'; // NEW MODEL if (isNaN(quantity) || quantity <= 0) { alert("Пожалуйста, укажите корректное количество (больше 0)."); @@ -560,7 +599,7 @@ CATALOG_TEMPLATE = ''' return; } - const cartItemId = `${product.name}-${color}`; + const cartItemId = `${product.name}-${color}-${model}`; // Include model in unique ID const existingItemIndex = cart.findIndex(item => item.id === cartItemId); if (existingItemIndex > -1) { @@ -572,7 +611,8 @@ CATALOG_TEMPLATE = ''' price: product.price, photo: product.photos && product.photos.length > 0 ? product.photos[0] : null, quantity: quantity, - color: color + color: color, + model: model // NEW }); } @@ -616,13 +656,18 @@ CATALOG_TEMPLATE = ''' const photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60.png?text=N/A'; - const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : ''; + + let variantText = []; + if (item.color !== 'N/A') variantText.push(`Цвет: ${item.color}`); + if (item.model !== 'N/A') variantText.push(`Модель: ${item.model}`); // NEW MODEL DISPLAY + + const variantDisplay = variantText.length > 0 ? ` (${variantText.join(', ')})` : ''; return `
${item.name}
- ${item.name}${colorText} + ${item.name}${variantDisplay}

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

${itemTotal.toFixed(2)} ${currencyCode} @@ -709,6 +754,9 @@ CATALOG_TEMPLATE = ''' const existingNoResults = grid.querySelector('.no-results-message'); if (existingNoResults) existingNoResults.remove(); + + const allProductElements = document.querySelectorAll('.products-grid .product'); + if (allProductElements.length === 0 && products.length > 0) return; // Wait for initial render if products exist document.querySelectorAll('.products-grid .product').forEach(productElement => { const name = productElement.getAttribute('data-name'); @@ -726,16 +774,18 @@ CATALOG_TEMPLATE = ''' } }); - if (visibleProducts === 0 && products.length > 0) { - const p = document.createElement('p'); - p.className = 'no-results-message'; - p.textContent = 'По вашему запросу товары не найдены.'; - grid.appendChild(p); - } else if (products.length === 0 && !grid.querySelector('.no-results-message')) { - const p = document.createElement('p'); - p.className = 'no-results-message'; - p.textContent = 'Товары пока не добавлены.'; - grid.appendChild(p); + if (visibleProducts === 0) { + if (products.length > 0) { + const p = document.createElement('p'); + p.className = 'no-results-message'; + p.textContent = 'По вашему запросу товары не найдены.'; + grid.appendChild(p); + } else if (!grid.querySelector('.no-results-message')) { + const p = document.createElement('p'); + p.className = 'no-results-message'; + p.textContent = 'Товары пока не добавлены.'; + grid.appendChild(p); + } } } @@ -761,7 +811,7 @@ CATALOG_TEMPLATE = ''' const newPlaceholder = document.createElement('div'); newPlaceholder.id = 'notification-placeholder'; newPlaceholder.style.position = 'fixed'; - newPlaceholder.style.bottom = '80px'; + newPlaceholder.style.bottom = '70px'; newPlaceholder.style.left = '50%'; newPlaceholder.style.transform = 'translateX(-50%)'; newPlaceholder.style.zIndex = '1002'; @@ -812,8 +862,8 @@ CATALOG_TEMPLATE = ''' PRODUCT_DETAIL_TEMPLATE = '''
-

{{ product['name'] }}

-
+

{{ product['name'] }}

+
{% if product.get('photos') and product['photos']|length > 0 %} {% for photo in product['photos'] %} @@ -833,21 +883,35 @@ PRODUCT_DETAIL_TEMPLATE = '''
{% if product.get('photos') and product['photos']|length > 1 %}
-
-
+
+
{% endif %}
-
+

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

-

Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}

+

Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}

Описание:
{{ product.get('description', 'Описание отсутствует.')|replace('\\n', '
')|safe }}

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

Доступные цвета/варианты: {{ colors|select('ne', '')|join(', ') }}

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

Доступные модели/объемы: {{ models|select('ne', '')|join(', ') }}

+ {% endif %} +
+
+
+ ''' ORDER_TEMPLATE = ''' @@ -857,34 +921,36 @@ ORDER_TEMPLATE = ''' Заказ №{{ order.id }} - Мобильный мир - + @@ -897,10 +963,22 @@ ORDER_TEMPLATE = '''

Товары в заказе

{% for item in order.cart %} + {% set variant_info = [] %} + {% if item.color and item.color != 'N/A' %} + {% set _ = variant_info.append('Цвет: ' + item.color) %} + {% endif %} + {% if item.model and item.model != 'N/A' %} + {% set _ = variant_info.append('Модель: ' + item.model) %} + {% endif %} + {% set variant_display = variant_info | join(', ') %} +
{{ item.name }}
- {{ item.name }} {% if item.color != 'N/A' %}({{ item.color }}){% endif %} + {{ item.name }} + {% if variant_display %} + ({{ variant_display }}) + {% endif %} {{ "%.2f"|format(item.price) }} {{ currency_code }} × {{ item.quantity }}
@@ -917,6 +995,7 @@ ORDER_TEMPLATE = '''

Статус заказа

+

Ваш текущий статус: {{ status_map_ru.get(order.status, order.status) }}

Этот заказ был оформлен без входа в систему.

Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.

@@ -925,7 +1004,7 @@ ORDER_TEMPLATE = '''
- ← Вернуться в каталог + ← Вернуться в каталог
-
- Мобильный мир Logo -

Админ-панель Мобильный мир

+
+ Мобильный мир Logo +

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

+
+
+ Каталог +
- Перейти в каталог
@@ -1064,20 +1184,7 @@ ADMIN_TEMPLATE = ''' {% endwith %}
-

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

-
-
- -
-
- -
-
-

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

-
- -
-

История заказов (Предполагается Алматинское время)

+

История заказов

{% if orders %} {% set sorted_orders = orders | sort(attribute='created_at', reverse=true) %} @@ -1085,25 +1192,34 @@ ADMIN_TEMPLATE = ''' {% set current_status = order.get('status', 'new') %} {% set total_items = order.cart | sum(attribute='quantity') %}
- + Заказ №{{ order.id }} {{ status_map_ru.get(current_status, current_status) }} - - Дата: {{ order.created_at }} | Итого: {{ "%.2f"|format(order.total_price) }} {{ currency_code }} ({{ total_items }} шт.) + + Дата: {{ order.created_at }} | Итого: {{ "%.2f"|format(order.total_price) }} {{ currency_code }} -
+

Состав заказа:

-
+
{% for item in order.cart %} {% set item_price = item.price * item.quantity %} + {% set variant_info = [] %} + {% if item.color and item.color != 'N/A' %} + {% set _ = variant_info.append('Цвет: ' + item.color) %} + {% endif %} + {% if item.model and item.model != 'N/A' %} + {% set _ = variant_info.append('Модель: ' + item.model) %} + {% endif %} + {% set variant_display = variant_info | join(', ') %} +
{{ item.name }}
{{ item.name }} - {% if item.color != 'N/A' %}

Цвет: {{ item.color }}

{% endif %} + {% if variant_display %}

{{ variant_display }}

{% endif %}
{{ item.quantity }} × {{ "%.2f"|format(item.price) }} {{ currency_code }} = {{ "%.2f"|format(item_price) }} {{ currency_code }} @@ -1116,17 +1232,17 @@ ADMIN_TEMPLATE = '''
-
+ - - {% for status_key, status_value in status_map_ru.items() %} {% endfor %} - - Просмотр + + Просмотр
@@ -1144,7 +1260,7 @@ ADMIN_TEMPLATE = '''

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

Добавить новую категорию -
+
@@ -1158,12 +1274,12 @@ ADMIN_TEMPLATE = ''' {% if categories %}
{% for category in categories %} -
- {{ category }} +
+ {{ category }} - +
{% endfor %} @@ -1176,9 +1292,10 @@ ADMIN_TEMPLATE = '''
-

Информация

-

Управление пользователями отключено, так как сайт не требует входа.

-

Заказы создаются анонимно и должны быть подтверждены через WhatsApp.

+

Информация о магазине

+

Магазин работает в режиме каталога, заказы оформляются анонимно и должны быть подтверждены через WhatsApp.

+

Адрес магазина: {{ store_address }}

+

Валюта: {{ currency_name }} ({{ currency_code }})

@@ -1187,7 +1304,7 @@ ADMIN_TEMPLATE = '''

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

Добавить новый товар -
+
@@ -1205,22 +1322,35 @@ ADMIN_TEMPLATE = ''' - + +

Варианты:

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

@@ -1240,11 +1370,11 @@ ADMIN_TEMPLATE = ''' Фото {% else %} - Нет фото + Нет фото {% endif %}
-

+

{{ product['name'] }} {% if product.get('in_stock', True) %} В наличии @@ -1257,12 +1387,11 @@ ADMIN_TEMPLATE = '''

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

Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}

-

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

+

Описание: {{ product.get('description', 'N/A')[:100] }}{% if product.get('description', '')|length > 100 %}...{% 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 %} + {% set models = product.get('models', []) %} +

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

+

Модели: {{ models|select('ne', '')|join(', ') if models|select('ne', '')|list|length > 0 else 'Нет' }}

@@ -1275,8 +1404,8 @@ ADMIN_TEMPLATE = '''
-
-

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

+ {% 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 %} {% else %} -
+
- +
{% endif %}
- -
-
+ + + +
+ {% set current_models = product.get('models', []) %} + {% if current_models and current_models|select('ne', '')|list|length > 0 %} + {% for model in current_models %} + {% if model.strip() %} +
+ + +
+ {% endif %} + {% endfor %} + {% else %} +
+ + +
+ {% endif %} +
+ + + +
@@ -1354,40 +1508,48 @@ ADMIN_TEMPLATE = ''' } } - function addColorInput(containerId) { + function addVariantInput(containerId, fieldName, placeholder) { const container = document.getElementById(containerId); if (container) { + // Check if there's only one empty placeholder left, and remove it before adding a new one + const existingInputs = container.querySelectorAll('input[name="' + fieldName + '"]'); + if (existingInputs.length === 1 && existingInputs[0].value.trim() === '') { + existingInputs[0].value = ''; // Just ensure it's clean and let the user fill it + } + const newInputGroup = document.createElement('div'); - newInputGroup.className = 'color-input-group'; + newInputGroup.className = 'variant-input-group'; newInputGroup.innerHTML = ` - - + + `; container.appendChild(newInputGroup); - const newInput = newInputGroup.querySelector('input[name="colors"]'); + const newInput = newInputGroup.querySelector('input[name="' + fieldName + '"]'); if (newInput) { newInput.focus(); } } } - function removeColorInput(button) { - const group = button.closest('.color-input-group'); + function removeVariantInput(button) { + const group = button.closest('.variant-input-group'); if (group) { const container = group.parentNode; + const fieldName = group.querySelector('input').name; + const placeholder = fieldName === 'colors' ? 'Например: Цвет' : 'Например: Модель'; + group.remove(); + // If all inputs for this field are removed, add an empty placeholder back if (container && container.children.length === 0) { const placeholderGroup = document.createElement('div'); - placeholderGroup.className = 'color-input-group'; + placeholderGroup.className = 'variant-input-group'; placeholderGroup.innerHTML = ` - - + + `; container.appendChild(placeholderGroup); } - } else { - console.warn("Could not find parent .color-input-group for remove button"); } } @@ -1427,10 +1589,14 @@ def product_detail(index): except IndexError: logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}") return "Товар не найден или отсутствует в наличии.", 404 + + # Pass the sorted index back so the JS function can correctly call openQuantityModal + products_index = index return render_template_string( PRODUCT_DETAIL_TEMPLATE, product=product, + products_index=products_index, repo_id=REPO_ID, currency_code=CURRENCY_CODE ) @@ -1457,13 +1623,18 @@ def create_order(): if price < 0 or quantity <= 0: raise ValueError("Invalid price or quantity") total_price += price * quantity + + photo_name = item.get('photo') + photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo_name}" if photo_name else "https://via.placeholder.com/60x60.png?text=N/A" + processed_cart.append({ "name": item['name'], "price": price, "quantity": quantity, "color": item.get('color', 'N/A'), - "photo": item.get('photo'), - "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=N/A" + "model": item.get('model', 'N/A'), # NEW MODEL + "photo": photo_name, + "photo_url": photo_url }) except (ValueError, TypeError) as e: logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}") @@ -1507,6 +1678,7 @@ def view_order(order_id): return render_template_string(ORDER_TEMPLATE, order=order, + status_map_ru=STATUS_MAP_RU, repo_id=REPO_ID, currency_code=CURRENCY_CODE) @@ -1599,6 +1771,7 @@ def admin(): category = request.form.get('category') photos_files = request.files.getlist('photos') colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] + models = [m.strip() for m in request.form.getlist('models') if m.strip()] # NEW MODELS in_stock = 'in_stock' in request.form is_top = 'is_top' in request.form @@ -1671,7 +1844,7 @@ def admin(): 'id': str(uuid.uuid4()), 'name': name, 'price': price, 'description': description, 'category': category if category in categories else 'Без категории', - 'photos': photos_list, 'colors': colors, + 'photos': photos_list, 'colors': colors, 'models': models, # NEW MODELS 'in_stock': in_stock, 'is_top': is_top } products.append(new_product) @@ -1706,6 +1879,7 @@ def admin(): 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['models'] = [m.strip() for m in request.form.getlist('models') if m.strip()] # NEW MODELS product_to_edit['in_stock'] = 'in_stock' in request.form product_to_edit['is_top'] = 'is_top' in request.form @@ -1861,36 +2035,11 @@ def admin(): orders=display_orders, status_map_ru=STATUS_MAP_RU, repo_id=REPO_ID, - currency_code=CURRENCY_CODE + store_address=STORE_ADDRESS, + currency_code=CURRENCY_CODE, + currency_name=CURRENCY_NAME ) -@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: - 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') - load_data() - else: - flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error') - except Exception as e: - 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()