Hub / app.py
Kgshop's picture
Update app.py
ba0a25f verified
from flask import Flask, render_template_string, request, redirect, url_for, flash, send_file
import json
import os
import logging
import threading
import time
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError
from werkzeug.utils import secure_filename
import uuid
import html
import random
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart")
DATA_FILE = 'data_zzirix.json'
REPO_ID = "Kgshop/clients"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
LOGO_URL = "https://huggingface.co/spaces/Kgshop/Zzirixadm/resolve/main/Picsart_25-03-20_15-38-36-600.jpg"
logging.basicConfig(level=logging.INFO)
def initialize_data_structure(data):
if not isinstance(data, dict):
data = {'categories': [], 'products': [], 'orders': {}}
data.setdefault('categories', [])
data.setdefault('products', [])
data.setdefault('orders', {})
if any(p.get('category') == 'Без категории' for p in data['products']) and 'Без категории' not in data['categories']:
data['categories'].append('Без категории')
for product in data['products']:
if 'id' not in product:
product['id'] = str(uuid.uuid4())
product.setdefault('name', 'Без названия')
product.setdefault('description', '')
product.setdefault('category', 'Без категории')
product.setdefault('price', 0.0)
product.setdefault('colors', [])
product.setdefault('models', [])
product.setdefault('photos', [])
product.setdefault('in_stock', True)
product.setdefault('is_top', False)
if not product['photos'] and 'media' in product and product['media']:
product['photos'] = [m['filename'] for m in product['media'] if m['type'] == 'photo']
if 'media' in product:
del product['media']
data['categories'] = sorted(list(set(data['categories'])), key=lambda x: (x != 'Без категории', x))
return data
def load_data():
try:
if not os.path.exists(DATA_FILE) or os.path.getsize(DATA_FILE) == 0:
logging.info(f"{DATA_FILE} не найден или пуст, попытка загрузки с HF.")
download_db_from_hf()
if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
logging.info("Данные успешно загружены из JSON.")
else:
data = {'products': [], 'categories': [], 'orders': {}}
logging.warning("Файл базы данных пуст или не существует после всех попыток, создается пустая структура.")
return initialize_data_structure(data)
except (json.JSONDecodeError, FileNotFoundError) as e:
logging.error(f"Ошибка при чтении {DATA_FILE}: {e}. Создается пустая структура.")
return initialize_data_structure({})
except RepositoryNotFoundError as e:
logging.error(f"Репозиторий HF не найден: {e}. Создается локальная база данных.")
return initialize_data_structure({})
except Exception as e:
logging.error(f"Ошибка при загрузке данных: {e}")
return initialize_data_structure({})
def save_data(data):
try:
temp_file = DATA_FILE + '.tmp'
with open(temp_file, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
os.replace(temp_file, DATA_FILE)
upload_db_to_hf()
logging.info("Данные сохранены и выгружены в HF")
except Exception as e:
logging.error(f"Ошибка при сохранении данных: {e}")
if os.path.exists(temp_file):
os.remove(temp_file)
raise
def upload_db_to_hf():
if not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN_WRITE не установлен. Пропуск выгрузки.")
return
try:
api = HfApi()
api.upload_file(
path_or_fileobj=DATA_FILE,
path_in_repo=DATA_FILE,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Резервная копия {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info("База данных выгружена в Hugging Face")
except Exception as e:
logging.error(f"Ошибка при выгрузке базы данных: {e}")
def download_db_from_hf():
if not HF_TOKEN_READ:
logging.warning("HF_TOKEN_READ не установлен. Пропуск загрузки.")
if not os.path.exists(DATA_FILE):
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(initialize_data_structure({}), f)
return
try:
hf_hub_download(
repo_id=REPO_ID,
filename=DATA_FILE,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True
)
logging.info("База данных загружена с Hugging Face")
except RepositoryNotFoundError:
logging.error(f"Репозиторий {REPO_ID} не найден. Пропускаем загрузку.")
if not os.path.exists(DATA_FILE):
logging.info("Создается пустой файл базы данных, так как загрузка не удалась и файл не существует.")
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(initialize_data_structure({}), f)
except Exception as e:
logging.error(f"Ошибка при загрузке базы данных: {e}")
if not os.path.exists(DATA_FILE):
logging.info("Создается пустой файл базы данных, так как загрузка не удалась и файл не существует.")
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(initialize_data_structure({}), f)
def periodic_backup():
while True:
time.sleep(3600)
logging.info("Запуск периодического резервного копирования.")
try:
data = load_data()
save_data(data)
except Exception as e:
logging.error(f"Ошибка во время периодического резервного копирования: {e}")
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'webp', 'mp4', 'mov', 'webm'}
BASE_STYLE = '''
:root {
--primary-color: #3B82F6;
--primary-dark-color: #2563eb;
--accent-color: #10b981;
--accent-dark-color: #059669;
--danger-color: #ef4444;
--danger-dark-color: #dc2626;
--background-light: linear-gradient(135deg, #f0f2f5, #e0e5ec);
--background-dark: linear-gradient(135deg, #1f2937, #374151);
--card-background-light: #ffffff;
--card-background-dark: #2d3748;
--text-color-light: #2d3748;
--text-color-dark: #e2e8f0;
--secondary-text-color-light: #718096;
--secondary-text-color-dark: #a0aec0;
--border-color-light: #e2e8f0;
--border-color-dark: #4a5568;
--shadow-light: 0 8px 25px rgba(0, 0, 0, 0.1);
--shadow-hover-light: 0 12px 35px rgba(0, 0, 0, 0.18);
--shadow-dark: 0 8px 25px rgba(0, 0, 0, 0.35);
--shadow-hover-dark: 0 12px 35px rgba(0, 0, 0, 0.5);
--border-radius-large: 22px;
--border-radius-medium: 12px;
--border-radius-small: 8px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background: var(--background-light);
color: var(--text-color-light);
line-height: 1.6;
transition: background 0.3s, color 0.3s;
min-height: 100vh;
display: flex;
flex-direction: column;
}
body.dark-mode {
background: var(--background-dark);
color: var(--text-color-dark);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 30px;
flex-grow: 1;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: none;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
border-radius: var(--border-radius-large);
background: var(--card-background-light);
}
body.dark-mode .header {
border-bottom-color: var(--border-color-dark);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
background: var(--card-background-dark);
}
.header-info {
display: flex;
align-items: center;
}
.header-logo {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease, box-shadow 0.3s ease;
margin-right: 15px;
}
.header-logo:hover {
transform: scale(1.08);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}
.header h1 {
font-size: 2rem;
font-weight: 800;
color: var(--primary-color);
}
.theme-toggle {
background: none;
border: none;
font-size: 1.8rem;
cursor: pointer;
color: var(--secondary-text-color-light);
transition: color 0.3s ease, transform 0.2s ease;
padding: 5px;
}
body.dark-mode .theme-toggle {
color: var(--secondary-text-color-dark);
}
.theme-toggle:hover {
color: var(--accent-color);
transform: rotate(30deg);
}
.flash {
padding: 18px;
margin-bottom: 25px;
border-radius: var(--border-radius-medium);
font-weight: 500;
border: 1px solid transparent;
font-size: 1.1rem;
}
.flash.success {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-dark-color);
}
.flash.error {
background-color: var(--danger-color);
color: white;
border-color: var(--danger-dark-color);
}
.filters-container {
margin: 30px 0;
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
}
.search-container {
margin: 25px 0;
text-align: center;
}
#search-input {
width: 90%;
max-width: 600px;
padding: 14px 22px;
font-size: 1rem;
border: none;
border-radius: var(--border-radius-medium);
outline: none;
box-shadow: var(--shadow-light);
transition: all 0.3s ease;
background-color: var(--card-background-light);
color: var(--text-color-light);
}
body.dark-mode #search-input {
border-color: var(--border-color-dark);
background-color: var(--card-background-dark);
color: var(--text-color-dark);
box-shadow: var(--shadow-dark);
}
#search-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(59, 130, 246, 0.4);
}
.category-filter {
padding: 12px 25px;
border: none;
border-radius: var(--border-radius-medium);
background-color: var(--card-background-light);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 1rem;
font-weight: 500;
color: var(--text-color-light);
box-shadow: var(--shadow-light);
}
body.dark-mode .category-filter {
border-color: var(--border-color-dark);
background-color: var(--card-background-dark);
color: var(--text-color-dark);
box-shadow: var(--shadow-dark);
}
.category-filter.active, .category-filter:hover {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: var(--shadow-hover-light);
}
body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
box-shadow: var(--shadow-hover-dark);
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 25px;
padding: 15px;
}
.product {
background: var(--card-background-light);
border-radius: var(--border-radius-large);
padding: 20px;
box-shadow: var(--shadow-light);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
}
body.dark-mode .product {
background: var(--card-background-dark);
box-shadow: var(--shadow-dark);
}
.product:hover {
transform: translateY(-10px) scale(1.03);
box-shadow: var(--shadow-hover-light);
}
body.dark-mode .product:hover {
box-shadow: var(--shadow-hover-dark);
}
.product-image {
width: 100%;
aspect-ratio: 1;
background-color: #f7f7f7;
border-radius: var(--border-radius-medium);
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
cursor: pointer;
}
body.dark-mode .product-image {
background-color: #343e4d;
}
.product-image img, .product-image video {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
border-radius: var(--border-radius-medium);
}
.product-image img:hover, .product-image video:hover {
transform: scale(1.05);
}
.product h2 {
font-size: 1.2rem;
font-weight: 700;
margin: 8px 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-color-light);
}
body.dark-mode .product h2 {
color: var(--text-color-dark);
}
.product-price {
font-size: 1.3rem;
color: var(--danger-color);
font-weight: 700;
text-align: center;
margin: 8px 0 12px;
}
.product-description {
font-size: 0.9rem;
color: var(--secondary-text-color-light);
text-align: center;
margin-bottom: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
body.dark-mode .product-description {
color: var(--secondary-text-color-dark);
}
.product-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: auto;
}
.product-button {
display: block;
width: 100%;
padding: 12px;
border: none;
border-radius: var(--border-radius-medium);
background-color: var(--primary-color);
color: white;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-align: center;
text-decoration: none;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.product-button:hover {
background-color: var(--primary-dark-color);
box-shadow: 0 6px 15px rgba(59, 130, 246, 0.4);
transform: translateY(-2px);
}
.add-to-cart {
background-color: var(--accent-color);
box-shadow: 0 3px 10px rgba(16, 185, 129, 0.2);
}
.add-to-cart:hover {
background-color: var(--accent-dark-color);
box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4);
}
#cart-button {
position: fixed;
bottom: 30px;
right: 30px;
background-color: var(--danger-color);
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 1.5rem;
cursor: pointer;
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
#cart-button:hover {
background-color: var(--danger-dark-color);
transform: translateY(-5px) scale(1.08);
box-shadow: 0 12px 35px rgba(239, 68, 68, 0.5);
}
.cart-count {
position: absolute;
top: -5px;
right: -5px;
background-color: var(--accent-color);
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 0.8rem;
min-width: 22px;
text-align: center;
font-weight: 600;
}
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.6);
backdrop-filter: blur(8px);
overflow-y: auto;
padding: 20px;
}
.modal-content {
background: var(--card-background-light);
margin: 50px auto;
padding: 40px;
border-radius: var(--border-radius-large);
width: 95%;
max-width: 750px;
box-shadow: var(--shadow-hover-light);
animation: fadeInScale 0.3s ease-out;
max-height: 90vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
position: relative;
}
body.dark-mode .modal-content {
background: var(--card-background-dark);
color: var(--text-color-dark);
box-shadow: var(--shadow-hover-dark);
}
@keyframes fadeInScale {
from { opacity: 0; transform: translateY(-30px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.close {
position: absolute;
top: 20px;
right: 25px;
font-size: 2rem;
color: var(--secondary-text-color-light);
cursor: pointer;
transition: color 0.3s, transform 0.2s;
}
.close:hover {
color: var(--danger-color);
transform: rotate(90deg);
}
body.dark-mode .close {
color: var(--secondary-text-color-dark);
}
body.dark-mode .close:hover {
color: var(--danger-color);
}
.modal h2 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 25px;
text-align: center;
}
.cart-item {
display: flex;
align-items: center;
padding: 18px 0;
border-bottom: 1px solid var(--border-color-light);
}
body.dark-mode .cart-item {
border-bottom-color: var(--border-color-dark);
}
.cart-item:last-child {
border-bottom: none;
}
.cart-item img {
width: 70px;
height: 70px;
object-fit: contain;
border-radius: var(--border-radius-small);
margin-right: 20px;
background-color: #f7f7f7;
}
body.dark-mode .cart-item img {
background-color: #343e4d;
}
.cart-item-details {
flex-grow: 1;
}
.cart-item-details strong {
font-size: 1.2rem;
font-weight: 600;
}
.cart-item-details p {
font-size: 0.95rem;
color: var(--secondary-text-color-light);
}
body.dark-mode .cart-item-details p {
color: var(--secondary-text-color-dark);
}
.cart-item-total {
font-size: 1.1rem;
font-weight: 700;
color: var(--danger-color);
}
.quantity-input, .color-select, .model-select {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color-light);
border-radius: var(--border-radius-medium);
font-size: 1rem;
margin: 10px 0;
background-color: var(--card-background-light);
color: var(--text-color-light);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
}
body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .model-select {
border-color: var(--border-color-dark);
background-color: var(--card-background-dark);
color: var(--text-color-dark);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
}
.modal-buttons {
margin-top: 30px;
display: flex;
justify-content: flex-end;
gap: 15px;
}
.modal-buttons .product-button {
width: auto;
padding: 12px 25px;
}
.clear-cart {
background-color: var(--danger-color);
}
.clear-cart:hover {
background-color: var(--danger-dark-color);
box-shadow: 0 6px 15px rgba(239, 68, 68, 0.4);
}
.order-button {
background-color: var(--accent-color);
}
.order-button:hover {
background-color: var(--accent-dark-color);
box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4);
}
.swiper-container {
max-width: 500px;
margin: 0 auto 30px;
border-radius: var(--border-radius-large);
overflow: hidden;
box-shadow: var(--shadow-light);
}
body.dark-mode .swiper-container {
box-shadow: var(--shadow-dark);
}
.swiper-slide {
background-color: #f7f7f7;
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
body.dark-mode .swiper-slide {
background-color: #343e4d;
}
.swiper-slide img, .swiper-slide video {
max-width: 100%;
max-height: 350px;
object-fit: contain;
}
.swiper-button-next, .swiper-button-prev {
color: var(--primary-color) !important;
background-color: rgba(255,255,255,0.9);
border-radius: 50%;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
font-size: 1.8rem;
}
.swiper-button-next:hover, .swiper-button-prev:hover {
background-color: #fff;
}
body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev {
background-color: rgba(45, 55, 72, 0.9);
}
body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover {
background-color: #2d3748;
}
.swiper-pagination-bullet {
background-color: var(--primary-color) !important;
}
.product-detail-modal-inner {
background: transparent;
padding: 0;
margin: 0;
box-shadow: none;
position: static;
}
body.dark-mode .product-detail-modal-inner {
background: transparent;
box-shadow: none;
}
.product-detail-modal-inner h2 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 25px;
text-align: center;
color: var(--text-color-light);
}
body.dark-mode .product-detail-modal-inner h2 {
color: var(--text-color-dark);
}
.product-detail-modal-inner p {
margin-bottom: 10px;
font-size: 1rem;
}
.product-detail-modal-inner p strong {
color: var(--text-color-light);
}
body.dark-mode .product-detail-modal-inner p strong {
color: var(--text-color-dark);
}
.product-detail-modal-inner .price {
font-size: 1.8rem;
color: var(--danger-color);
font-weight: 700;
margin-bottom: 20px;
text-align: center;
}
.product-detail-modal-inner .description {
margin-bottom: 20px;
white-space: pre-wrap;
}
.product-detail-modal-actions {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 30px;
}
.product-detail-modal-actions .product-button {
width: auto;
padding: 12px 25px;
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.header h1 {
font-size: 1.6rem;
}
.category-filter {
font-size: 0.85rem;
padding: 8px 15px;
}
.products-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 18px;
}
.product h2 {
font-size: 1.05rem;
}
.product-price {
font-size: 1.15rem;
}
.product-description {
font-size: 0.8rem;
}
.product-button {
font-size: 0.9rem;
padding: 10px;
}
#cart-button {
width: 50px;
height: 50px;
font-size: 1.2rem;
bottom: 20px;
right: 20px;
}
.modal-content {
margin: 20px auto;
padding: 25px;
max-height: 95vh;
}
.close {
font-size: 1.6rem;
top: 15px;
right: 18px;
}
.modal h2 {
font-size: 1.6rem;
}
.cart-item img {
width: 55px;
height: 55px;
}
.product-detail-modal-inner h2 {
font-size: 2rem;
}
.product-detail-modal-inner .price {
font-size: 1.6rem;
}
.product-detail-modal-actions {
flex-direction: column;
gap: 10px;
}
}
'''
@app.route('/')
def catalog():
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
products.sort(key=lambda p: p['name'].lower())
return render_template_string('''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZZIRIX - сотовые аксессуары оптом</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
<style>''' + BASE_STYLE + '''</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
<h1>Каталог ZZIRIX</h1>
</div>
<button class="theme-toggle" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="filters-container">
<button class="category-filter active" data-category="all">Все категории</button>
{% for category in categories %}
<button class="category-filter" data-category="{{ category | lower }}">{{ category }}</button>
{% endfor %}
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Поиск товаров...">
</div>
<div class="products-grid" id="products-grid">
{% for product in products %}
<div class="product"
data-name="{{ product['name']|lower }}"
data-description="{{ product['description']|lower }}"
data-category="{{ product.get('category', 'Без категории')|lower }}">
<a href="javascript:void(0)" onclick="openProductDetailModal('{{ product.id }}')" class="product-image">
{% if product.get('photos') and product['photos']|length > 0 %}
{% set filename = product['photos'][0] %}
{% if filename.endswith(('.mp4', '.mov', '.webm')) %}
<video preload="metadata" muted loop autoplay playsinline><source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}" type="video/mp4"></video>
{% else %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}"
alt="{{ product['name'] }}"
loading="lazy">
{% endif %}
{% else %}
<img src="https://via.placeholder.com/200x200?text=No+Image"
alt="Нет изображения"
loading="lazy">
{% endif %}
</a>
<h2>{{ product['name'] }}</h2>
<div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
<p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
<div class="product-actions">
<button class="product-button" onclick="openProductDetailModal('{{ product.id }}')">Подробнее</button>
<button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
</div>
</div>
{% else %}
<p style="grid-column: 1 / -1; text-align: center; padding: 40px 0;">Товаров пока нет. Загляните позже!</p>
{% endfor %}
</div>
</div>
<div id="quantityModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('quantityModal')">×</span>
<h2>Укажите количество, цвет и модель</h2>
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
<select id="colorSelect" class="color-select"></select>
<select id="modelSelect" class="model-select"></select>
<div class="modal-buttons">
<button class="product-button order-button" onclick="confirmAddToCart()">Добавить</button>
</div>
</div>
</div>
<div id="cartModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('cartModal')">×</span>
<h2>Корзина</h2>
<div id="cartContent"></div>
<div style="margin-top: 20px; text-align: right;">
<strong style="font-size: 1.2rem;">Итого: <span id="cartTotal">0</span> с</strong>
<div class="modal-buttons">
<button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
<button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
</div>
</div>
</div>
</div>
<div id="productDetailModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('productDetailModal')">×</span>
<div class="product-detail-modal-inner">
<h2 id="detailProductName"></h2>
<div class="swiper-container" style="max-width: 500px; margin: 0 auto 30px;">
<div class="swiper-wrapper" id="detailProductMedia">
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
<p><strong>Категория:</strong> <span id="detailProductCategory" style="color: var(--primary-color);"></span></p>
<p class="price" id="detailProductPrice"></p>
<p class="description"><strong>Описание:</strong> <span id="detailProductDescription"></span></p>
<p><strong>Доступные цвета:</strong> <span id="detailProductColors" style="color: var(--secondary-text-color-light);"></span></p>
<p><strong>Доступные модели:</strong> <span id="detailProductModels" style="color: var(--secondary-text-color-light);"></span></p>
<div class="product-detail-modal-actions">
<button class="product-button add-to-cart" id="detailAddToCartButton">В корзину</button>
</div>
</div>
</div>
</div>
<button id="cart-button" onclick="openCartModal()">
<i class="fas fa-shopping-cart"></i>
<span id="cart-count" class="cart-count">0</span>
</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
<script>
const products = {{ products|tojson }};
let selectedProductId = null;
let detailSwiperInstance = null;
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const icon = document.querySelector('.theme-toggle i');
icon.classList.toggle('fa-moon');
icon.classList.toggle('fa-sun');
localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
}
if (localStorage.getItem('theme') === 'dark') {
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');
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = "none";
if (modalId === 'productDetailModal' && detailSwiperInstance) {
detailSwiperInstance.destroy(true, true);
detailSwiperInstance = null;
}
}
function openQuantityModal(productId) {
selectedProductId = productId;
const product = products.find(p => p.id === productId);
if (!product) {
alert('Ошибка: Товар не найден.');
return;
}
const colorSelect = document.getElementById('colorSelect');
colorSelect.innerHTML = '';
if (product.colors && product.colors.length > 0) {
product.colors.forEach(color => {
const option = document.createElement('option');
option.value = color;
option.text = color;
colorSelect.appendChild(option);
});
} else {
const option = document.createElement('option');
option.value = 'Не указан';
option.text = 'Цвет не указан';
colorSelect.appendChild(option);
}
const modelSelect = document.getElementById('modelSelect');
modelSelect.innerHTML = '';
if (product.models && product.models.length > 0) {
product.models.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.text = model;
modelSelect.appendChild(option);
});
} else {
const option = document.createElement('option');
option.value = 'Не указана';
option.text = 'Модель не указана';
modelSelect.appendChild(option);
}
document.getElementById('quantityModal').style.display = 'block';
document.getElementById('quantityInput').value = 1;
}
function confirmAddToCart() {
if (selectedProductId === null) return;
const quantityInput = document.getElementById('quantityInput');
const quantity = parseInt(quantityInput.value) || 1;
const color = document.getElementById('colorSelect').value;
const model = document.getElementById('modelSelect').value;
if (quantity <= 0) {
alert("Укажите количество больше 0.");
quantityInput.focus();
return;
}
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
const product = products.find(p => p.id === selectedProductId);
const cartItemId = `${product.id}-${color}-${model}`;
const existingItem = cart.find(item => item.cartId === cartItemId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({
cartId: cartItemId,
productId: product.id,
name: product.name,
price: product.price,
photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
quantity: quantity,
color: color,
model: model
});
}
localStorage.setItem('cart', JSON.stringify(cart));
closeModal('quantityModal');
updateCartButton();
alert(`"${product.name}" добавлен в корзину.`);
}
function updateCartButton() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
document.getElementById('cart-count').textContent = cartCount;
document.getElementById('cart-button').style.display = cartCount > 0 ? 'flex' : 'none';
}
function openCartModal() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartContent = document.getElementById('cartContent');
let total = 0;
cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center; color: var(--secondary-text-color-light);">Корзина пуста</p>' : cart.map((item, index) => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60?text=No+Image';
return `
<div class="cart-item">
<img src="${photoSrc}" alt="${item.name}">
<div class="cart-item-details">
<strong>${item.name}</strong>
<p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p>
</div>
<span class="cart-item-total">${itemTotal.toFixed(2)} с</span>
</div>
`;
}).join('');
document.getElementById('cartTotal').textContent = total.toFixed(2);
document.getElementById('cartModal').style.display = 'block';
}
function orderViaWhatsApp() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert("Корзина пуста!");
return;
}
let total = 0;
let orderText = "Здравствуйте, меня интересует заказ:%0A%0A";
cart.forEach((item, index) => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
orderText += `${index + 1}. ${item.name}%0AКоличество: ${item.quantity}%0AЦена: ${item.price.toFixed(2)} с%0AЦвет: ${item.color}%0AМодель: ${item.model}%0A--%0A`;
});
orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
clearCart();
}
function clearCart() {
localStorage.removeItem('cart');
openCartModal();
updateCartButton();
}
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = "none";
if (detailSwiperInstance) {
detailSwiperInstance.destroy(true, true);
detailSwiperInstance = null;
}
}
}
document.getElementById('search-input').addEventListener('input', filterProducts);
document.querySelectorAll('.filters-container .category-filter').forEach(filter => {
filter.addEventListener('click', function() {
document.querySelectorAll('.filters-container .category-filter').forEach(f => f.classList.remove('active'));
this.classList.add('active');
filterProducts();
});
});
function filterProducts() {
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
const activeCategory = document.querySelector('.category-filter.active').dataset.category.toLowerCase();
document.querySelectorAll('.product').forEach(product => {
const name = product.getAttribute('data-name');
const description = product.getAttribute('data-description');
const category = product.getAttribute('data-category');
const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm));
const matchesCategory = activeCategory === 'all' || category === activeCategory;
product.style.display = matchesSearch && matchesCategory ? 'flex' : 'none';
});
}
function openProductDetailModal(productId) {
selectedProductId = productId;
const product = products.find(p => p.id === productId);
if (!product) {
alert('Ошибка: Товар не найден.');
return;
}
document.getElementById('detailProductName').textContent = product.name;
document.getElementById('detailProductCategory').textContent = product.category || 'Без категории';
document.getElementById('detailProductPrice').textContent = `${product.price.toFixed(2)} с`;
document.getElementById('detailProductDescription').textContent = product.description;
document.getElementById('detailProductColors').textContent = product.colors && product.colors.length > 0 ? product.colors.join(', ') : 'Нет цветов';
document.getElementById('detailProductModels').textContent = product.models && product.models.length > 0 ? product.models.join(', ') : 'Нет моделей';
document.getElementById('detailAddToCartButton').onclick = () => openQuantityModal(product.id);
const mediaContainer = document.getElementById('detailProductMedia');
mediaContainer.innerHTML = '';
if (product.photos && product.photos.length > 0) {
product.photos.forEach(filename => {
const slide = document.createElement('div');
slide.className = 'swiper-slide swiper-zoom-container';
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
const mediaUrl = `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${filename}`;
if (filename.endsWith(('.mp4', '.mov', '.webm'))) {
slide.innerHTML = `<video controls preload="metadata" style="max-width: 100%; max-height: 350px; object-fit: contain;"><source src="${mediaUrl}" type="video/mp4"></video>`;
} else {
slide.innerHTML = `<img src="${mediaUrl}" alt="${product.name}" style="max-width: 100%; max-height: 350px; object-fit: contain;">`;
}
mediaContainer.appendChild(slide);
});
} else {
const slide = document.createElement('div');
slide.className = 'swiper-slide swiper-zoom-container';
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
slide.innerHTML = `<img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">`;
mediaContainer.appendChild(slide);
}
document.getElementById('productDetailModal').style.display = 'block';
if (detailSwiperInstance) {
detailSwiperInstance.destroy(true, true);
}
detailSwiperInstance = new Swiper('#productDetailModal .swiper-container', {
slidesPerView: 1,
spaceBetween: 20,
loop: true,
grabCursor: true,
pagination: { el: '#productDetailModal .swiper-pagination', clickable: true },
navigation: { nextEl: '#productDetailModal .swiper-button-next', prevEl: '#productDetailModal .swiper-button-prev' },
zoom: { maxRatio: 3 },
on: {
init: function () {
this.slides.forEach(slide => {
const img = slide.querySelector('img');
if (img && !img.complete) {
img.onload = () => detailSwiperInstance.update();
}
const video = slide.querySelector('video');
if (video) {
video.onloadedmetadata = () => detailSwiperInstance.update();
}
});
},
resize: function() {
this.update();
}
}
});
detailSwiperInstance.update();
}
updateCartButton();
</script>
</body>
</html>
''',
products=products,
categories=categories,
repo_id=REPO_ID,
LOGO_URL=LOGO_URL
)
@app.route('/category/<category_name>')
def catalog_by_category(category_name):
data = load_data()
products_all = data.get('products', [])
categories = data.get('categories', [])
products = [p for p in products_all if p.get('category', '').lower() == category_name.lower()]
products.sort(key=lambda p: p['name'].lower())
return render_template_string('''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ category_name }} - ZZIRIX</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
<style>''' + BASE_STYLE + '''</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
<h1>Каталог ZZIRIX</h1>
</div>
<button class="theme-toggle" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="filters-container">
<button class="category-filter" data-category="all" onclick="window.location.href='{{ url_for('catalog') }}'">Все категории</button>
{% for cat in categories %}
<button class="category-filter {% if cat | lower == category_name | lower %}active{% endif %}" data-category="{{ cat | lower }}" onclick="window.location.href='{{ url_for('catalog_by_category', category_name=cat | lower) }}'">{{ cat }}</button>
{% endfor %}
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Поиск товаров...">
</div>
<div class="products-grid" id="products-grid">
{% for product in products %}
<div class="product"
data-name="{{ product['name']|lower }}"
data-description="{{ product['description']|lower }}"
data-category="{{ product.get('category', 'Без категории')|lower }}">
<a href="javascript:void(0)" onclick="openProductDetailModal('{{ product.id }}')" class="product-image">
{% if product.get('photos') and product['photos']|length > 0 %}
{% set filename = product['photos'][0] %}
{% if filename.endswith(('.mp4', '.mov', '.webm')) %}
<video preload="metadata" muted loop autoplay playsinline><source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}" type="video/mp4"></video>
{% else %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ filename }}"
alt="{{ product['name'] }}"
loading="lazy">
{% endif %}
{% else %}
<img src="https://via.placeholder.com/200x200?text=No+Image"
alt="Нет изображения"
loading="lazy">
{% endif %}
</a>
<h2>{{ product['name'] }}</h2>
<div class="product-price">{{ "%.2f"|format(product['price']) }} с</div>
<p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
<div class="product-actions">
<button class="product-button" onclick="openProductDetailModal('{{ product.id }}')">Подробнее</button>
<button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">В корзину</button>
</div>
</div>
{% else %}
<p style="grid-column: 1 / -1; text-align: center; padding: 40px 0;">В этой категории пока нет товаров.</p>
{% endfor %}
</div>
</div>
<div id="quantityModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('quantityModal')">×</span>
<h2>Укажите количество, цвет и модель</h2>
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
<select id="colorSelect" class="color-select"></select>
<select id="modelSelect" class="model-select"></select>
<div class="modal-buttons">
<button class="product-button order-button" onclick="confirmAddToCart()">Добавить</button>
</div>
</div>
</div>
<div id="cartModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('cartModal')">×</span>
<h2>Корзина</h2>
<div id="cartContent"></div>
<div style="margin-top: 20px; text-align: right;">
<strong style="font-size: 1.2rem;">Итого: <span id="cartTotal">0</span> с</strong>
<div class="modal-buttons">
<button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
<button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
</div>
</div>
</div>
</div>
<div id="productDetailModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('productDetailModal')">×</span>
<div class="product-detail-modal-inner">
<h2 id="detailProductName"></h2>
<div class="swiper-container" style="max-width: 500px; margin: 0 auto 30px;">
<div class="swiper-wrapper" id="detailProductMedia">
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
<p><strong>Категория:</strong> <span id="detailProductCategory" style="color: var(--primary-color);"></span></p>
<p class="price" id="detailProductPrice"></p>
<p class="description"><strong>Описание:</strong> <span id="detailProductDescription"></span></p>
<p><strong>Доступные цвета:</strong> <span id="detailProductColors" style="color: var(--secondary-text-color-light);"></span></p>
<p><strong>Доступные модели:</strong> <span id="detailProductModels" style="color: var(--secondary-text-color-light);"></span></p>
<div class="product-detail-modal-actions">
<button class="product-button add-to-cart" id="detailAddToCartButton">В корзину</button>
</div>
</div>
</div>
</div>
<button id="cart-button" onclick="openCartModal()">
<i class="fas fa-shopping-cart"></i>
<span id="cart-count" class="cart-count">0</span>
</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
<script>
const products = {{ products_all|tojson }};
let selectedProductId = null;
let detailSwiperInstance = null;
function toggleTheme() {
document.body.classList.toggle('dark-mode');
const icon = document.querySelector('.theme-toggle i');
icon.classList.toggle('fa-moon');
icon.classList.toggle('fa-sun');
localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
}
if (localStorage.getItem('theme') === 'dark') {
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');
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = "none";
if (modalId === 'productDetailModal' && detailSwiperInstance) {
detailSwiperInstance.destroy(true, true);
detailSwiperInstance = null;
}
}
function openQuantityModal(productId) {
selectedProductId = productId;
const product = products.find(p => p.id === productId);
if (!product) {
alert('Ошибка: Товар не найден.');
return;
}
const colorSelect = document.getElementById('colorSelect');
colorSelect.innerHTML = '';
if (product.colors && product.colors.length > 0) {
product.colors.forEach(color => {
const option = document.createElement('option');
option.value = color;
option.text = color;
colorSelect.appendChild(option);
});
} else {
const option = document.createElement('option');
option.value = 'Не указан';
option.text = 'Цвет не указан';
colorSelect.appendChild(option);
}
const modelSelect = document.getElementById('modelSelect');
modelSelect.innerHTML = '';
if (product.models && product.models.length > 0) {
product.models.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.text = model;
modelSelect.appendChild(option);
});
} else {
const option = document.createElement('option');
option.value = 'Не указана';
option.text = 'Модель не указана';
modelSelect.appendChild(option);
}
document.getElementById('quantityModal').style.display = 'block';
document.getElementById('quantityInput').value = 1;
}
function confirmAddToCart() {
if (selectedProductId === null) return;
const quantityInput = document.getElementById('quantityInput');
const quantity = parseInt(quantityInput.value) || 1;
const color = document.getElementById('colorSelect').value;
const model = document.getElementById('modelSelect').value;
if (quantity <= 0) {
alert("Укажите количество больше 0.");
quantityInput.focus();
return;
}
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
const product = products.find(p => p.id === selectedProductId);
const cartItemId = `${product.id}-${color}-${model}`;
const existingItem = cart.find(item => item.cartId === cartItemId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({
cartId: cartItemId,
productId: product.id,
name: product.name,
price: product.price,
photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
quantity: quantity,
color: color,
model: model
});
}
localStorage.setItem('cart', JSON.stringify(cart));
closeModal('quantityModal');
updateCartButton();
alert(`"${product.name}" добавлен в корзину.`);
}
function updateCartButton() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
document.getElementById('cart-count').textContent = cartCount;
document.getElementById('cart-button').style.display = cartCount > 0 ? 'flex' : 'none';
}
function openCartModal() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartContent = document.getElementById('cartContent');
let total = 0;
cartContent.innerHTML = cart.length === 0 ? '<p style="text-align: center; color: var(--secondary-text-color-light);">Корзина пуста</p>' : cart.map((item, index) => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const photoSrc = item.photo ? `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/60x60?text=No+Image';
return `
<div class="cart-item">
<img src="${photoSrc}" alt="${item.name}">
<div class="cart-item-details">
<strong>${item.name}</strong>
<p>${item.price.toFixed(2)} с × ${item.quantity} (Цвет: ${item.color}, Модель: ${item.model})</p>
</div>
<span class="cart-item-total">${itemTotal.toFixed(2)} с</span>
</div>
`;
}).join('');
document.getElementById('cartTotal').textContent = total.toFixed(2);
document.getElementById('cartModal').style.display = 'block';
}
function orderViaWhatsApp() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert("Корзина пуста!");
return;
}
let total = 0;
let orderText = "Здравствуйте, меня интересует заказ:%0A%0A";
cart.forEach((item, index) => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
orderText += `${index + 1}. ${item.name}%0AКоличество: ${item.quantity}%0AЦена: ${item.price.toFixed(2)} с%0AЦвет: ${item.color}%0AМодель: ${item.model}%0A--%0A`;
});
orderText += `*Итого к оплате: ${total.toFixed(2)} с*%0A%0AЖду подтверждения заказа.`;
window.open(`https://api.whatsapp.com/send?phone=996705665777&text=${orderText}`, '_blank');
clearCart();
}
function clearCart() {
localStorage.removeItem('cart');
openCartModal();
updateCartButton();
}
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = "none";
if (detailSwiperInstance) {
detailSwiperInstance.destroy(true, true);
detailSwiperInstance = null;
}
}
}
document.getElementById('search-input').addEventListener('input', filterProducts);
function filterProducts() {
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
const activeCategory = "{{ category_name | lower }}";
document.querySelectorAll('.product').forEach(product => {
const name = product.getAttribute('data-name');
const description = product.getAttribute('data-description');
const matchesSearch = (name && name.includes(searchTerm)) || (description && description.includes(searchTerm));
product.style.display = matchesSearch ? 'flex' : 'none';
});
}
function openProductDetailModal(productId) {
selectedProductId = productId;
const product = products.find(p => p.id === productId);
if (!product) {
alert('Ошибка: Товар не найден.');
return;
}
document.getElementById('detailProductName').textContent = product.name;
document.getElementById('detailProductCategory').textContent = product.category || 'Без категории';
document.getElementById('detailProductPrice').textContent = `${product.price.toFixed(2)} с`;
document.getElementById('detailProductDescription').textContent = product.description;
document.getElementById('detailProductColors').textContent = product.colors && product.colors.length > 0 ? product.colors.join(', ') : 'Нет цветов';
document.getElementById('detailProductModels').textContent = product.models && product.models.length > 0 ? product.models.join(', ') : 'Нет моделей';
document.getElementById('detailAddToCartButton').onclick = () => openQuantityModal(product.id);
const mediaContainer = document.getElementById('detailProductMedia');
mediaContainer.innerHTML = '';
if (product.photos && product.photos.length > 0) {
product.photos.forEach(filename => {
const slide = document.createElement('div');
slide.className = 'swiper-slide swiper-zoom-container';
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
const mediaUrl = `https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${filename}`;
if (filename.endsWith(('.mp4', '.mov', '.webm'))) {
slide.innerHTML = `<video controls preload="metadata" style="max-width: 100%; max-height: 350px; object-fit: contain;"><source src="${mediaUrl}" type="video/mp4"></video>`;
} else {
slide.innerHTML = `<img src="${mediaUrl}" alt="${product.name}" style="max-width: 100%; max-height: 350px; object-fit: contain;">`;
}
mediaContainer.appendChild(slide);
});
} else {
const slide = document.createElement('div');
slide.className = 'swiper-slide swiper-zoom-container';
slide.style = "display: flex; justify-content: center; align-items: center; border-radius: 10px;";
slide.innerHTML = `<img src="https://via.placeholder.com/300x300?text=No+Image" alt="No Image">`;
mediaContainer.appendChild(slide);
}
document.getElementById('productDetailModal').style.display = 'block';
if (detailSwiperInstance) {
detailSwiperInstance.destroy(true, true);
}
detailSwiperInstance = new Swiper('#productDetailModal .swiper-container', {
slidesPerView: 1,
spaceBetween: 20,
loop: true,
grabCursor: true,
pagination: { el: '#productDetailModal .swiper-pagination', clickable: true },
navigation: { nextEl: '#productDetailModal .swiper-button-next', prevEl: '#productDetailModal .swiper-button-prev' },
zoom: { maxRatio: 3 },
on: {
init: function () {
this.slides.forEach(slide => {
const img = slide.querySelector('img');
if (img && !img.complete) {
img.onload = () => detailSwiperInstance.update();
}
const video = slide.querySelector('video');
if (video) {
video.onloadedmetadata = () => detailSwiperInstance.update();
}
});
},
resize: function() {
this.update();
}
}
});
detailSwiperInstance.update();
}
updateCartButton();
</script>
</body>
</html>
''',
products=products,
categories=categories,
category_name=category_name,
products_all=products_all,
repo_id=REPO_ID,
LOGO_URL=LOGO_URL
)
@app.route('/admin', methods=['GET', 'POST'])
def admin():
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
if request.method == 'POST':
action = request.form.get('action')
api = HfApi()
if action == 'add_category':
category_name = request.form.get('category_name', '').strip()
if category_name and category_name not in categories:
categories.append(category_name)
save_data(data)
flash('Категория успешно добавлена.', 'success')
return redirect(url_for('admin'))
flash('Ошибка: Категория уже существует или не указано название.', 'error')
return redirect(url_for('admin'))
elif action == 'delete_category':
category_name_to_delete = request.form.get('category_name')
if category_name_to_delete in categories:
categories.remove(category_name_to_delete)
for product in products:
if product.get('category') == category_name_to_delete:
product['category'] = 'Без категории'
save_data(data)
flash('Категория удалена. Связанные товары теперь в категории "Без категории".', 'success')
return redirect(url_for('admin'))
flash('Ошибка: Категория не найдена.', 'error')
return redirect(url_for('admin'))
elif action == 'add':
name = request.form.get('name', '').strip()
price = request.form.get('price', '').strip()
description = request.form.get('description', '').strip()
category = request.form.get('category', 'Без категории').strip()
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()]
if not name or not price or not description:
flash('Ошибка: Заполните все обязательные поля (Название, Цена, Описание).', 'error')
return redirect(url_for('admin'))
try:
price = float(price.replace(',', '.'))
except ValueError:
flash('Ошибка: Неверный формат цены.', 'error')
return redirect(url_for('admin'))
photos_list = []
if photos_files and any(f.filename for f in photos_files):
uploads_dir = 'uploads'
os.makedirs(uploads_dir, exist_ok=True)
for i, photo in enumerate(photos_files[:10]):
if photo and allowed_file(photo.filename):
base, extension = os.path.splitext(photo.filename)
unique_filename = secure_filename(f"{name.replace(' ','_')}_{int(time.time())}_{i}{extension}")
temp_path = os.path.join(uploads_dir, unique_filename)
try:
photo.save(temp_path)
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"photos/{unique_filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Добавлено фото для товара {name}"
)
photos_list.append(unique_filename)
except Exception as e:
logging.error(f"Ошибка при загрузке фото {unique_filename}: {e}")
flash(f'Ошибка при загрузке фото {photo.filename}: {e}', 'error')
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
new_product = {
'id': str(uuid.uuid4()),
'name': name,
'price': price,
'description': description,
'category': category if category in categories else 'Без категории',
'photos': photos_list,
'colors': colors,
'models': models,
'in_stock': True,
'is_top': False
}
products.append(new_product)
save_data(data)
flash('Товар успешно добавлен.', 'success')
return redirect(url_for('admin'))
elif action == 'edit':
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('Ошибка: Товар не найден для редактирования.', 'error')
return redirect(url_for('admin'))
name = request.form.get('name', '').strip()
price = request.form.get('price', '').strip()
description = request.form.get('description', '').strip()
category = request.form.get('category', 'Без категории').strip()
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()]
if not name or not price or not description:
flash('Ошибка: Заполните все обязательные поля (Название, Цена, Описание) при редактировании.', 'error')
return redirect(url_for('admin'))
try:
price = float(price.replace(',', '.'))
except ValueError:
flash('Ошибка: Неверный формат цены при редактировании.', 'error')
return redirect(url_for('admin'))
product_to_edit['name'] = name
product_to_edit['price'] = price
product_to_edit['description'] = description
product_to_edit['category'] = category if category in categories else 'Без категории'
product_to_edit['colors'] = colors
product_to_edit['models'] = models
if photos_files and any(f.filename for f in photos_files):
new_photos_list = []
uploads_dir = 'uploads'
os.makedirs(uploads_dir, exist_ok=True)
for old_filename in product_to_edit.get('photos', []):
try:
api.delete_file(path_in_repo=f"photos/{old_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
logging.info(f"Старое фото {old_filename} удалено из HF.")
except Exception as e:
logging.error(f"Не удалось удалить старое фото {old_filename} из HF: {e}")
for i, photo in enumerate(photos_files[:10]):
if photo and allowed_file(photo.filename):
base, extension = os.path.splitext(photo.filename)
unique_filename = secure_filename(f"{name.replace(' ','_')}_{int(time.time())}_{i}{extension}")
temp_path = os.path.join(uploads_dir, unique_filename)
try:
photo.save(temp_path)
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"photos/{unique_filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Обновлено фото для товара {name}"
)
new_photos_list.append(unique_filename)
except Exception as e:
logging.error(f"Ошибка при загрузке нового фото {unique_filename}: {e}")
flash(f'Ошибка при загрузке нового фото {photo.filename}: {e}', 'error')
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
product_to_edit['photos'] = new_photos_list
save_data(data)
flash('Товар успешно отредактирован.', 'success')
return redirect(url_for('admin'))
elif action == 'delete':
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:
for filename in product_to_delete.get('photos', []):
try:
api.delete_file(path_in_repo=f"photos/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
logging.info(f"Фото {filename} удалено из HF.")
except Exception as e:
logging.error(f"Не удалось удалить фото {filename} из HF: {e}")
data['products'] = [p for p in products if p.get('id') != product_id]
save_data(data)
flash('Товар удален.', 'success')
return redirect(url_for('admin'))
flash('Ошибка: Товар не найден для удаления.', 'error')
return redirect(url_for('admin'))
products.sort(key=lambda p: p['name'].lower())
return render_template_string('''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель ZZIRIX</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>''' + BASE_STYLE + '''
body {
font-family: 'Roboto', sans-serif;
background: var(--background-light);
color: var(--text-color-light);
padding: 20px;
line-height: 1.6;
transition: background 0.3s, color 0.3s;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid var(--border-color-light);
margin-bottom: 20px;
}
.header-logo {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
margin-right: 15px;
}
.header-logo:hover {
transform: scale(1.05);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
h1, h2 {
font-weight: 700;
margin-bottom: 25px;
color: var(--text-color-light);
}
body.dark-mode h1, body.dark-mode h2 {
color: var(--text-color-dark);
}
form {
background: var(--card-background-light);
padding: 30px;
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-light);
margin-bottom: 30px;
transition: background 0.3s, box-shadow 0.3s;
}
body.dark-mode form {
background: var(--card-background-dark);
box-shadow: var(--shadow-dark);
}
label {
font-weight: 600;
margin-top: 18px;
margin-bottom: 5px;
display: block;
color: var(--text-color-light);
}
body.dark-mode label {
color: var(--text-color-dark);
}
input, textarea, select {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border: 1px solid var(--border-color-light);
border-radius: var(--border-radius-medium);
font-size: 1rem;
transition: all 0.3s ease;
background-color: var(--card-background-light);
color: var(--text-color-light);
}
body.dark-mode input, body.dark-mode textarea, body.dark-mode select {
border-color: var(--border-color-dark);
background-color: var(--card-background-dark);
color: var(--text-color-dark);
}
input:focus, textarea:focus, select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
outline: none;
}
button {
padding: 12px 22px;
border: none;
border-radius: var(--border-radius-medium);
background-color: var(--primary-color);
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 15px;
font-size: 1rem;
}
button:hover {
background-color: var(--primary-dark-color);
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
transform: translateY(-2px);
}
.delete-button {
background-color: var(--danger-color);
margin-left: 10px;
}
.delete-button:hover {
background-color: var(--danger-dark-color);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
}
.product-list, .category-list {
display: grid;
gap: 20px;
}
.product-item, .category-item {
background: var(--card-background-light);
padding: 20px;
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-light);
transition: background 0.3s, box-shadow 0.3s;
}
body.dark-mode .product-item, body.dark-mode .category-item {
background: var(--card-background-dark);
box-shadow: var(--shadow-dark);
}
.product-item h3, .category-item h3 {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-color-light);
}
body.dark-mode .product-item h3, body.dark-mode .category-item h3 {
color: var(--text-color-dark);
}
.product-item p {
font-size: 0.95rem;
margin-bottom: 5px;
color: var(--secondary-text-color-light);
}
body.dark-mode .product-item p {
color: var(--secondary-text-color-dark);
}
.edit-form {
margin-top: 20px;
padding: 20px;
background: var(--background-light);
border-radius: var(--border-radius-medium);
box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
}
body.dark-mode .edit-form {
background: #3a4250;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.15);
}
.color-input-group, .model-input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.color-input-group input, .model-input-group input {
flex-grow: 1;
margin-bottom: 0;
}
.remove-input-btn {
background-color: #ccc;
color: #555;
padding: 8px 12px;
border-radius: var(--border-radius-small);
font-size: 0.8rem;
margin-top: 0;
flex-shrink: 0;
}
.remove-input-btn:hover {
background-color: #bbb;
box-shadow: none;
transform: none;
}
.add-color-btn, .add-model-btn {
background-color: var(--accent-color);
width: auto;
padding: 10px 18px;
margin-bottom: 15px;
}
.add-color-btn:hover, .add-model-btn:hover {
background-color: var(--accent-dark-color);
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
}
.product-photos-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
margin-bottom: 15px;
}
.product-photos-preview img, .product-photos-preview video {
max-width: 90px;
height: auto;
border-radius: var(--border-radius-small);
border: 1px solid var(--border-color-light);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
body.dark-mode .product-photos-preview img, body.dark-mode .product-photos-preview video {
border-color: var(--border-color-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.admin-section-title {
margin-top: 40px;
margin-bottom: 20px;
font-size: 1.8rem;
color: var(--primary-color);
}
body.dark-mode .admin-section-title {
color: var(--primary-color);
}
.db-management-buttons button {
margin-right: 10px;
}
.search-admin-container {
margin: 20px 0;
text-align: left;
}
#admin-search-input {
width: 100%;
max-width: 400px;
margin-left: 0;
border: 1px solid var(--border-color-light);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
}
body.dark-mode #admin-search-input {
border-color: var(--border-color-dark);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
<h1>Админ-панель</h1>
</div>
<a href="{{ url_for('catalog') }}" style="text-decoration: none;" class="product-button">Перейти в каталог</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2 class="admin-section-title">Добавление товара</h2>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="add">
<label>Название товара:</label>
<input type="text" name="name" required>
<label>Цена:</label>
<input type="number" name="price" step="0.01" required>
<label>Описание:</label>
<textarea name="description" rows="4" required></textarea>
<label>Категория:</label>
<select name="category">
<option value="Без категории">Без категории</option>
{% for category in categories %}
<option value="{{ category }}">{{ category }}</option>
{% endfor %}
</select>
<label>Фотографии и видео (до 10):</label>
<input type="file" name="photos" accept="image/*,video/*" multiple>
<label>Цвета:</label>
<div id="color-inputs">
<div class="color-input-group">
<input type="text" name="colors" placeholder="Например: Красный">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
</div>
</div>
<button type="button" class="add-color-btn" onclick="addInput('color-inputs', 'colors', 'Цвет')">Добавить цвет</button>
<label>Модели телефона:</label>
<div id="model-inputs">
<div class="model-input-group">
<input type="text" name="models" placeholder="Например: iPhone 14">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
</div>
</div>
<button type="button" class="add-model-btn" onclick="addInput('model-inputs', 'models', 'Модель')">Добавить модель</button>
<button type="submit">Добавить товар</button>
</form>
<h2 class="admin-section-title">Управление категориями</h2>
<form method="POST">
<input type="hidden" name="action" value="add_category">
<label>Название категории:</label>
<input type="text" name="category_name" required>
<button type="submit">Добавить</button>
</form>
<h2>Список категорий</h2>
<div class="category-list">
{% for category in categories %}
<div class="category-item">
<h3>{{ category }}</h3>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="delete_category">
<input type="hidden" name="category_name" value="{{ category }}">
<button type="submit" class="delete-button">Удалить</button>
</form>
</div>
{% endfor %}
</div>
<h2 class="admin-section-title">Управление базой данных</h2>
<div class="db-management-buttons">
<form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
<button type="submit">Создать копию (вручную)</button>
</form>
<form method="GET" action="{{ url_for('download') }}" style="display: inline;">
<button type="submit">Скачать базу (JSON)</button>
</form>
</div>
<h2 class="admin-section-title">Список товаров</h2>
<div class="search-admin-container">
<input type="text" id="admin-search-input" placeholder="Поиск товаров в админ-панели...">
</div>
<div class="product-list" id="admin-product-list">
{% for product in products %}
<div class="product-item"
data-name="{{ product['name']|lower }}"
data-description="{{ product['description']|lower }}"
data-category="{{ product.get('category', 'без категории')|lower }}">
<h3>{{ product['name'] }}</h3>
<p><strong>ID:</strong> {{ product.id }}</p>
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
<p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} с</p>
<p><strong>Описание:</strong> {{ product['description'] }}</p>
<p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
<p><strong>Модели:</strong> {{ product.get('models', ['Нет моделей'])|join(', ') }}</p>
{% if product.get('photos') and product['photos']|length > 0 %}
<div class="product-photos-preview">
{% for photo in product['photos'] %}
{% if photo.endswith(('.mp4', '.mov', '.webm')) %}
<video src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}"
alt="{{ product['name'] }}"
loading="lazy"
controls
style="max-width: 90px; height: auto; border-radius: var(--border-radius-small); border: 1px solid var(--border-color-light); box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
</video>
{% else %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}?r={{ random.randint(1,1000) }}"
alt="{{ product['name'] }}"
loading="lazy">
{% endif %}
{% endfor %}
</div>
{% endif %}
<details>
<summary style="font-weight: 500; cursor: pointer; color: var(--primary-color); margin-top: 15px;">Редактировать</summary>
<form method="POST" enctype="multipart/form-data" class="edit-form">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="product_id" value="{{ product.id }}">
<label>Название:</label>
<input type="text" name="name" value="{{ product['name'] }}" required>
<label>Цена:</label>
<input type="number" name="price" step="0.01" value="{{ "%.2f"|format(product['price']) }}" required>
<label>Описание:</label>
<textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
<label>Категория:</label>
<select name="category">
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
{% for category in categories %}
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
{% endfor %}
</select>
<label>Фотографии и видео (заменят существующие, до 10):</label>
<input type="file" name="photos" accept="image/*,video/*" multiple>
<label>Цвета:</label>
<div id="edit-color-inputs-{{ product.id }}">
{% for color in product.get('colors', []) %}
<div class="color-input-group">
<input type="text" name="colors" value="{{ color }}">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
</div>
{% endfor %}
<div class="color-input-group">
<input type="text" name="colors" placeholder="Например: Новый цвет">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
</div>
</div>
<button type="button" class="add-color-btn" onclick="addInput('edit-color-inputs-{{ product.id }}', 'colors', 'Цвет')">Добавить цвет</button>
<label>Модели телефона:</label>
<div id="edit-model-inputs-{{ product.id }}">
{% for model in product.get('models', []) %}
<div class="model-input-group">
<input type="text" name="models" value="{{ model }}">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
</div>
{% endfor %}
<div class="model-input-group">
<input type="text" name="models" placeholder="Например: Новая модель">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
</div>
</div>
<button type="button" class="add-model-btn" onclick="addInput('edit-model-inputs-{{ product.id }}', 'models', 'Модель')">Добавить модель</button>
<button type="submit">Сохранить</button>
</form>
</details>
<form method="POST" style="margin-top: 15px;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="product_id" value="{{ product.id }}">
<button type="submit" class="delete-button">Удалить товар</button>
</form>
</div>
{% endfor %}
</div>
</div>
<script>
function addInput(containerId, name, placeholderPrefix) {
const container = document.getElementById(containerId);
const newInput = document.createElement('div');
newInput.className = `${name.replace('s', '')}-input-group`;
newInput.innerHTML = `
<input type="text" name="${name}" placeholder="Например: ${placeholderPrefix}">
<button type="button" class="remove-input-btn" onclick="this.parentNode.remove()">X</button>
`;
container.appendChild(newInput);
}
document.getElementById('admin-search-input').addEventListener('input', filterAdminProducts);
function filterAdminProducts() {
const searchTerm = document.getElementById('admin-search-input').value.toLowerCase().trim();
document.querySelectorAll('.product-item').forEach(productItem => {
const name = productItem.getAttribute('data-name');
const description = productItem.getAttribute('data-description');
const category = productItem.getAttribute('data-category');
const matchesSearch = (name && name.includes(searchTerm)) ||
(description && description.includes(searchTerm)) ||
(category && category.includes(searchTerm));
productItem.style.display = matchesSearch ? 'block' : 'none';
});
}
</script>
</body>
</html>
''',
products=products,
categories=categories,
repo_id=REPO_ID,
LOGO_URL=LOGO_URL,
random=random
)
@app.route('/backup', methods=['POST'])
def backup():
try:
data = load_data()
save_data(data)
flash('Резервная копия успешно создана и загружена.', 'success')
return redirect(url_for('admin'))
except Exception as e:
logging.error(f"Ошибка при ручном создании резервной копии: {e}")
flash(f'Ошибка при создании резервной копии: {e}', 'error')
return redirect(url_for('admin'))
@app.route('/download', methods=['GET'])
def download():
try:
download_db_from_hf()
if os.path.exists(DATA_FILE):
return send_file(DATA_FILE, as_attachment=True, mimetype='application/json', download_name='data_zzirix_backup.json')
flash('Файл базы данных не найден после попытки скачивания.', 'error')
return redirect(url_for('admin'))
except Exception as e:
logging.error(f"Ошибка при скачивании базы данных: {e}")
flash(f'Ошибка при скачивании базы данных: {e}', 'error')
return redirect(url_for('admin'))
if __name__ == '__main__':
uploads_dir = 'uploads'
os.makedirs(uploads_dir, exist_ok=True)
logging.info("Начальная проверка и обновление структуры данных...")
try:
current_data = load_data()
save_data(current_data)
logging.info("Структура данных проверена и при необходимости обновлена.")
except Exception as e:
logging.error(f"Ошибка во время начальной проверки/обновления структуры данных: {e}")
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
logging.info("Запуск Flask приложения...")
app.run(debug=True, host='0.0.0.0', port=7860)