import os
import io
import base64
import json
import logging
import threading
import time
import math
import re
import urllib.parse
from datetime import datetime, timedelta, timezone
from uuid import uuid4
import random
import string
from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session, make_response
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
import requests
load_dotenv()
app = Flask(__name__)
app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login'
DATA_FILE = 'data.json'
SYNC_FILES = [DATA_FILE]
REPO_ID = "Kgshop/bcnew"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
ALMATY_TZ = timezone(timedelta(hours=6))
db_lock = threading.RLock()
CURRENCIES = {
'KGS': 'Кыргызский сом',
'KZT': 'Казахстанский тенге',
'UAH': 'Украинская гривна',
'RUB': 'Российский рубль',
'USD': 'Доллар США',
'EUR': 'Евро',
'UZS': 'Узбекский сум'
}
COLOR_SCHEMES = {
'default': 'Бирюзовый (по умолч.)',
'forest': 'Лесной зеленый',
'ocean': 'Глубокий синий',
'sunset': 'Закатный оранжевый',
'lavender': 'Лавандовый',
'vintage': 'Винтажный',
'dark': 'Полночь (тёмная)',
'cosmic': 'Космическая ночь',
'minty': 'Свежая мята',
'mocha': 'Кофейный мокко',
'crimson': 'Багровый рассвет',
'solar': 'Солнечная вспышка',
'cyberpunk': 'Киберпанк неон',
'neon': 'Неоновая вспышка',
'pastel': 'Пастельный (светлый)',
'emerald': 'Изумрудный город',
'gold': 'Роскошное золото',
'sakura': 'Цветение сакуры (светлый)',
'arctic': 'Арктический лед (светлый)',
'volcano': 'Магма',
'monochrome_light': 'Классика (Светлая)',
'monochrome_dark': 'Классика (Темная)',
'nord': 'Скандинавский Норд',
'dracula': 'Дракула (Темный)',
'ruby': 'Глубокий Рубин',
'sapphire': 'Королевский Сапфир',
'amethyst': 'Аметистовый блеск'
}
ICONS = {
'fa-link': 'Ссылка (вебсайт)',
'fa-phone': 'Телефон',
'fa-whatsapp': 'WhatsApp',
'fa-telegram': 'Telegram',
'fa-instagram': 'Instagram',
'fa-envelope': 'Email',
'fa-map-marker-alt': 'Локация/Адрес',
'fa-youtube': 'YouTube',
'fa-tiktok': 'TikTok'
}
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
pass
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
files_to_download = [specific_file] if specific_file else SYNC_FILES
all_successful = True
for file_name in files_to_download:
success = False
for attempt in range(retries + 1):
try:
hf_hub_download(
repo_id=REPO_ID,
filename=file_name,
repo_type="dataset",
token=token_to_use,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
resume_download=False
)
success = True
break
except RepositoryNotFoundError:
return False
except HfHubHTTPError as e:
if e.response.status_code == 404:
if attempt == 0 and not os.path.exists(file_name):
try:
if file_name == DATA_FILE:
with open(file_name, 'w', encoding='utf-8') as f:
json.dump({}, f)
except Exception:
pass
success = False
break
else:
pass
except requests.exceptions.RequestException:
pass
except Exception:
pass
if attempt < retries:
time.sleep(delay)
if not success:
all_successful = False
return all_successful
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
for file_name in files_to_upload:
if os.path.exists(file_name):
try:
api.upload_file(
path_or_fileobj=file_name,
path_in_repo=file_name,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Sync {file_name} {datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception:
pass
except Exception:
pass
def periodic_backup():
backup_interval = 1800
while True:
time.sleep(backup_interval)
upload_db_to_hf()
def load_data():
with db_lock:
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, dict):
data = {}
except (FileNotFoundError, json.JSONDecodeError):
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, dict):
data = {}
except (FileNotFoundError, json.JSONDecodeError):
data = {}
else:
data = {}
return data
def save_data(data):
with db_lock:
try:
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
except Exception:
pass
upload_db_to_hf(specific_file=DATA_FILE)
def get_env_data(env_id):
with db_lock:
all_data = load_data()
default_settings = {
"vcard_firstname": "Имя",
"vcard_lastname": "Фамилия",
"vcard_job": "Специалист",
"organization_name": "Моя Компания",
"currency_code": "KGS",
"chat_avatar": None,
"color_scheme": "default",
"admin_password_enabled": False,
"admin_password": "",
"categories_as_lines": False,
"about_text": "Привет! Это моя онлайн-визитка.",
"enable_cart": False,
"order_messenger": "whatsapp",
"order_contact": ""
}
env_data = all_data.get(env_id, {})
if not env_data:
env_data = {
'products': [], 'categories':[], 'blocks':[], 'orders': [],
'settings': default_settings
}
if 'products' not in env_data: env_data['products'] = []
if 'categories' not in env_data: env_data['categories'] = []
if 'settings' not in env_data: env_data['settings'] = default_settings
if 'blocks' not in env_data: env_data['blocks'] = []
if 'orders' not in env_data: env_data['orders'] = []
settings_changed = False
for key, value in default_settings.items():
if key not in env_data['settings']:
env_data['settings'][key] = value
settings_changed = True
products_changed = False
for product in env_data['products']:
if 'product_id' not in product:
product['product_id'] = uuid4().hex
products_changed = True
if 'views' not in product:
product['views'] = 0
products_changed = True
if 'price' not in product:
product['price'] = 0.0
products_changed = True
if 'archived' not in product:
product['archived'] = False
products_changed = True
if 'description' not in product:
product['description'] = ""
products_changed = True
if 'variants' not in product:
product['variants'] = []
products_changed = True
if 'search_keywords' not in product:
product['search_keywords'] = ""
products_changed = True
if products_changed or settings_changed:
save_env_data(env_id, env_data)
return env_data
def save_env_data(env_id, env_data):
with db_lock:
all_data = load_data()
all_data[env_id] = env_data
save_data(all_data)
LANDING_PAGE_TEMPLATE = '''
Платформа Онлайн-Визиток
'''
LOGIN_TEMPLATE = '''
Вход в Админ-панель
Вход
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
'''
ADMHOSTO_TEMPLATE = '''
Главная Админ-панель
Управление Визитками
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
Существующие визитки
{% if environments %}
{% for env in environments %}
-
{% endfor %}
{% else %}
Пока не создано ни одной визитки.
{% endif %}
'''
CATALOG_TEMPLATE = '''
{{ settings.vcard_firstname }} {{ settings.vcard_lastname }} - Визитка
Сохранить в контакты
{% if blocks %}
{% endif %}
{% if products_json != '[]' %}
{% endif %}
Фильтр по цене
Мой QR-код
Отсканируйте, чтобы открыть визитку
'''
ADMIN_TEMPLATE = '''
Настройки Визитки - {{ settings.organization_name }}
Пожалуйста, подождите...
Идёт сохранение данных
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
Кнопки и Ссылки
Создать новую кнопку
{% if blocks %}
{% for block in blocks %}
{% if block.icon %}
{% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %}
{% endif %}
{{ block.title }}
({% if block.type == 'link' %}Ссылка{% elif block.type == 'pdf' %}PDF{% else %}Текст{% endif %})
{% if block.type == 'link' %}
{{ block.url }}{% endif %}
{% endfor %}
{% else %}
Кнопки еще не добавлены.
{% endif %}
Папки (для группировки фото/товаров)
Создать новую папку
{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}
Папок пока нет.
{% endif %}
Все фотографии и товары
Загрузить много фото на сайт
{% if paginated_products %}
{% for product in paginated_products %}
Отметить
{% if product.get('photos') %}
{% for p in product['photos'] %}

{% endfor %}
{% else %}

{% endif %}
Папка: {{ product.get('category', 'Без папки') if product.get('category') != 'Без категории' else 'Без папки' }}
Цена: {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}0{% endif %}
{% if product.get('description') %}
Описание: {{ product.get('description') }}
{% endif %}
{% if product.get('variants') %}
Варианты: {{ ', '.join(product.get('variants')) }}
{% endif %}
{% if product.get('search_keywords') %}
Ключевые слова: {{ product.get('search_keywords') }}
{% endif %}
{% if show_archive %}
{% else %}
{% endif %}
Изменение товара
{% endfor %}
{% if total_pages > 1 %}
{% endif %}
{% else %}
{% if show_archive %}Архив пуст.{% else %}Фотографии пока не добавлены.{% endif %}
{% endif %}
{% if show_archive %}
{% else %}
{% endif %}
'''
ORDER_TEMPLATE = '''
Заказ #{{ order['id'] }}
Покупатель: {{ order['customer_name'] }}
Телефон: {{ order['customer_phone'] }}
Продавец: {{ settings['organization_name'] }}
{% for cid, item in order['items'].items() %}
{% if item['photo'] %}

{% else %}

{% endif %}
{{ item['qty'] }} шт. × {{ "%.2f"|format(item['price']|float) }} {{ currency }}
{% if item.get('variant') %}
Вариант: {{ item['variant'] }}
{% endif %}
{{ "%.2f"|format(item['qty']|int * item['price']|float) }} {{ currency }}
{% endfor %}
Итого: {{ "%.2f"|format(order['total']|float) }} {{ currency }}
{% if send_link != "#" %}
Отправить продавцу ({{ messenger_name }})
{% endif %}
Вернуться в каталог
'''
ADMIN_ORDERS_TEMPLATE = '''
История заказов - {{ settings.organization_name }}
{% if orders %}
{% for order in orders %}
{% for cid, item in order['items'].items() %}
{% if item['photo'] %}

{% else %}
Фото
{% endif %}
{{ item['qty'] }} шт.
{% if item.get('variant') %}
Вариант: {{ item['variant'] }}{% endif %}
{{ "%.2f"|format(item['price']|float * item['qty']|int) }} {{ currency }}
{% endfor %}
Итого: {{ "%.2f"|format(order['total']|float) }} {{ currency }}
{% endfor %}
{% else %}
{% endif %}
'''
@app.route('/')
def index():
return render_template_string(LANDING_PAGE_TEMPLATE)
@app.route('/admhosto', methods=['GET'])
def admhosto():
data = load_data()
environments_data = []
for env_id, env_data in data.items():
settings = env_data.get('settings', {})
org_name = settings.get("organization_name", f"Визитка {env_id}")
environments_data.append({
"id": env_id,
"org_name": org_name,
"pwd_enabled": settings.get("admin_password_enabled", False),
"password": settings.get("admin_password", "")
})
environments_data.sort(key=lambda x: x['id'])
return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data)
@app.route('/admhosto/create', methods=['POST'])
def create_environment():
all_data = load_data()
while True:
new_id = ''.join(random.choices(string.digits, k=6))
if new_id not in all_data:
break
all_data[new_id] = {
'products': [], 'categories':[], 'blocks':[], 'orders': [],
'settings': {
"vcard_firstname": "Имя",
"vcard_lastname": "Фамилия",
"vcard_job": "Специалист",
"organization_name": "Моя Компания",
"currency_code": "KGS",
"chat_avatar": None,
"color_scheme": "default",
"admin_password_enabled": False,
"admin_password": "",
"categories_as_lines": False,
"about_text": "Привет! Это моя онлайн-визитка.",
"enable_cart": False,
"order_messenger": "whatsapp",
"order_contact": ""
}
}
save_data(all_data)
flash(f'Новая визитка с ID {new_id} успешно создана.', 'success')
return redirect(url_for('admhosto'))
@app.route('/admhosto/update_pwd/', methods=['POST'])
def update_env_pwd(env_id):
all_data = load_data()
if env_id in all_data:
pwd_enabled = 'pwd_enabled' in request.form
password = request.form.get('password', '').strip()
all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled
all_data[env_id]['settings']['admin_password'] = password
save_data(all_data)
flash(f'Пароль для визитки {env_id} обновлен.', 'success')
else:
flash(f'Визитка {env_id} не найдена.', 'error')
return redirect(url_for('admhosto'))
@app.route('/admhosto/delete/', methods=['POST'])
def delete_environment(env_id):
all_data = load_data()
if env_id in all_data:
del all_data[env_id]
save_data(all_data)
flash(f'Визитка {env_id} была удалена.', 'success')
else:
flash(f'Визитка {env_id} не найдена.', 'error')
return redirect(url_for('admhosto'))
@app.route('//login', methods=['GET', 'POST'])
def admin_login(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if not settings.get('admin_password_enabled'):
return redirect(url_for('admin', env_id=env_id))
if request.method == 'POST':
pwd = request.form.get('password', '')
if pwd == settings.get('admin_password', ''):
session[f'admin_auth_{env_id}'] = True
return redirect(url_for('admin', env_id=env_id))
else:
flash('Неверный пароль', 'error')
return render_template_string(LOGIN_TEMPLATE, env_id=env_id)
@app.route('//logout')
def admin_logout(env_id):
session.pop(f'admin_auth_{env_id}', None)
return redirect(url_for('admin_login', env_id=env_id))
@app.route('//vcard')
def download_vcard(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
blocks = data.get('blocks', [])
first_name = settings.get("vcard_firstname", "")
last_name = settings.get("vcard_lastname", "")
org = settings.get("organization_name", "")
title = settings.get("vcard_job", "")
note = settings.get("about_text", "").replace('\n', '\\n')
card_url = url_for('catalog', env_id=env_id, _external=True)
vcard = [
"BEGIN:VCARD",
"VERSION:3.0",
f"N:{last_name};{first_name};;;",
f"FN:{first_name} {last_name}".strip(),
]
if org: vcard.append(f"ORG:{org}")
if title: vcard.append(f"TITLE:{title}")
if note: vcard.append(f"NOTE:{note}")
vcard.append(f"URL;type=pref:{card_url}")
for b in blocks:
if b.get('type') == 'link' and b.get('url'):
url = b.get('url', '').strip()
if url.startswith('+') or url.replace('-', '').replace(' ', '').isdigit():
vcard.append(f"TEL;TYPE=CELL:{url}")
elif '@' in url and not url.startswith('http'):
vcard.append(f"EMAIL;TYPE=WORK:{url.replace('mailto:', '')}")
else:
vcard.append(f"URL:{url}")
vcard.append("END:VCARD")
vcard_str = "\r\n".join(vcard)
response = make_response(vcard_str)
response.headers["Content-Disposition"] = f"attachment; filename=contact_{env_id}.vcf"
response.headers["Content-Type"] = "text/vcard; charset=utf-8"
return response
@app.route('//catalog')
def catalog(env_id):
data = get_env_data(env_id)
all_products = data.get('products',[])
settings = data.get('settings', {})
blocks = data.get('blocks',[])
active_products = [p for p in all_products if not p.get('archived', False)]
product_categories = set(p.get('category', 'Без категории') for p in active_products)
admin_categories = set(data.get('categories',[]))
all_cat_names = sorted(list(product_categories.union(admin_categories)))
products_sorted_for_js = list(reversed(active_products))
products_by_category = {cat:[] for cat in all_cat_names}
for product in active_products:
products_by_category[product.get('category', 'Без категории')].append(product)
ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)]
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
return render_template_string(
CATALOG_TEMPLATE, ordered_categories=ordered_categories,
products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'),
settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks
)
@app.route('//checkout', methods=['POST'])
def checkout(env_id):
req_data = request.get_json()
env_data = get_env_data(env_id)
items = req_data.get('items', {})
if not items:
return jsonify({"success": False, "error": "Корзина пуста"})
total = sum(item.get('qty', 0) * item.get('price', 0) for item in items.values())
order = {
"id": ''.join(random.choices(string.digits, k=8)),
"date": datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S'),
"customer_name": req_data.get('customer_name', ''),
"customer_phone": req_data.get('customer_phone', ''),
"items": items,
"total": total,
"viewed": False
}
if 'orders' not in env_data:
env_data['orders'] = []
env_data['orders'].append(order)
save_env_data(env_id, env_data)
return jsonify({"success": True, "url": url_for('order_invoice', env_id=env_id, order_id=order['id'])})
@app.route('//order/')
def order_invoice(env_id, order_id):
env_data = get_env_data(env_id)
settings = env_data.get('settings', {})
orders = env_data.get('orders', [])
order = next((o for o in orders if o.get('id') == order_id), None)
if not order:
return "Заказ не найден", 404
currency = settings.get('currency_code', 'KGS')
text = f"Здравствуйте! Я оформил заказ #{order['id']} на сумму {order['total']} {currency}.\nСсылка на накладную: {request.url}"
encoded_text = urllib.parse.quote(text)
messenger = settings.get('order_messenger', 'whatsapp')
contact = settings.get('order_contact', '')
if messenger == 'whatsapp':
num = re.sub(r'[^0-9+]', '', contact)
send_link = f"https://wa.me/{num}?text={encoded_text}"
messenger_name = "WhatsApp"
elif messenger == 'telegram':
username = contact.replace('@', '').strip()
send_link = f"tg://resolve?domain={username}&text={encoded_text}"
messenger_name = "Telegram"
else:
send_link = "#"
messenger_name = "Нет"
return render_template_string(
ORDER_TEMPLATE,
order=order,
currency=currency,
send_link=send_link,
messenger_name=messenger_name,
repo_id=REPO_ID,
settings=settings,
env_id=env_id
)
@app.route('//admin', methods=['GET', 'POST'])
def admin(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
return redirect(url_for('admin_login', env_id=env_id))
products = data.get('products',[])
categories = data.get('categories',[])
blocks = data.get('blocks',[])
page = request.args.get('p', 1, type=int)
show_archive = request.args.get('tab') == 'archive'
if request.method == 'POST':
action = request.form.get('action')
try:
if action == 'bulk_add':
category = request.form.get('category', 'Без категории')
product_count = int(request.form.get('product_count', 0))
if HF_TOKEN_WRITE and product_count > 0:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
for i in range(product_count):
photos = request.files.getlist(f'photos_{i}')
price_str = request.form.get(f'price_{i}', '0')
desc_str = request.form.get(f'description_{i}', '').strip()
vars_str = request.form.get(f'variants_{i}', '')
words_str = request.form.get(f'search_keywords_{i}', '').strip()
try:
price_val = float(price_str)
except ValueError:
price_val = 0.0
variants_list = [v.strip() for v in vars_str.split(',') if v.strip()]
photo_filenames = []
for photo in photos:
if photo and photo.filename:
try:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: ext = '.jpg'
photo_filename = f"photo_{uuid4().hex[:12]}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
os.remove(temp_path)
photo_filenames.append(photo_filename)
except Exception: pass
if photo_filenames:
products.append({
'product_id': uuid4().hex,
'price': price_val,
'category': category,
'photos': photo_filenames,
'views': 0,
'archived': False,
'description': desc_str,
'variants': variants_list,
'search_keywords': words_str
})
data['products'] = products
save_env_data(env_id, data)
return "OK", 200
return "No photos", 400
elif action == 'bulk_delete':
product_ids = request.form.getlist('product_ids[]')
if product_ids:
to_delete_photos = []
new_products = []
for p in products:
if p.get('product_id') in product_ids:
to_delete_photos.extend(p.get('photos', []))
else:
new_products.append(p)
if to_delete_photos and HF_TOKEN_WRITE:
try:
api = HfApi()
paths = [f"photos/{p}" for p in to_delete_photos]
for chunk in [paths[x:x+50] for x in range(0, len(paths), 50)]:
api.delete_files(repo_id=REPO_ID, paths_in_repo=chunk, repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
data['products'] = new_products
save_env_data(env_id, data)
flash(f"Успешно удалено.", 'success')
elif action == 'bulk_archive':
product_ids = request.form.getlist('product_ids[]')
if product_ids:
for p in products:
if p.get('product_id') in product_ids:
p['archived'] = True
data['products'] = products
save_env_data(env_id, data)
flash(f"Отправлено в архив.", 'success')
elif action == 'bulk_unarchive':
product_ids = request.form.getlist('product_ids[]')
if product_ids:
for p in products:
if p.get('product_id') in product_ids:
p['archived'] = False
data['products'] = products
save_env_data(env_id, data)
flash(f"Восстановлено из архива.", 'success')
elif action == 'archive_product':
product_id = request.form.get('product_id')
for p in products:
if p.get('product_id') == product_id:
p['archived'] = True
break
data['products'] = products
save_env_data(env_id, data)
flash("Товар отправлен в архив.", 'success')
elif action == 'unarchive_product':
product_id = request.form.get('product_id')
for p in products:
if p.get('product_id') == product_id:
p['archived'] = False
break
data['products'] = products
save_env_data(env_id, data)
flash("Товар восстановлен из архива.", 'success')
elif action == 'add_block':
b_type = request.form.get('block_type')
b_title = request.form.get('block_title', '').strip()
b_icon = request.form.get('block_icon', '')
b_url = request.form.get('block_url', '').strip()
b_content = request.form.get('block_content', '').strip()
if b_type == 'link' and b_url:
if not b_url.startswith(('http://', 'https://', 'mailto:', 'tel:')) and not b_url.startswith('+'):
if '@' in b_url:
b_url = 'mailto:' + b_url
else:
b_url = 'https://' + b_url
elif b_url.startswith('+'):
b_url = 'tel:' + b_url.replace(' ', '').replace('-', '')
elif b_type == 'pdf':
b_url = ''
pdf_file = request.files.get('block_pdf')
if pdf_file and pdf_file.filename and HF_TOKEN_WRITE:
try:
ext = os.path.splitext(pdf_file.filename)[1].lower()
pdf_filename = f"pdf_{uuid4().hex[:8]}{ext}"
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, pdf_filename)
pdf_file.save(temp_path)
HfApi().upload_file(path_or_fileobj=temp_path, path_in_repo=f"pdfs/{pdf_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
os.remove(temp_path)
b_url = pdf_filename
except Exception as e:
flash(f"Ошибка загрузки PDF: {e}", 'error')
blocks.append({
'id': uuid4().hex[:8],
'type': b_type,
'title': b_title,
'icon': b_icon if b_type in ['link', 'pdf'] else '',
'url': b_url,
'content': b_content
})
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Кнопка добавлена.", "success")
elif action == 'edit_block':
b_id = request.form.get('block_id')
b_type = request.form.get('block_type')
b_title = request.form.get('block_title', '').strip()
b_icon = request.form.get('block_icon', '')
b_url = request.form.get('block_url', '').strip()
b_content = request.form.get('block_content', '').strip()
old_url = request.form.get('old_block_url', '')
for b in blocks:
if b.get('id') == b_id:
b['type'] = b_type
b['title'] = b_title
b['icon'] = b_icon if b_type in ['link', 'pdf'] else ''
b['content'] = b_content
if b_type == 'link':
if b_url:
if not b_url.startswith(('http://', 'https://', 'mailto:', 'tel:')) and not b_url.startswith('+'):
if '@' in b_url:
b_url = 'mailto:' + b_url
else:
b_url = 'https://' + b_url
elif b_url.startswith('+'):
b_url = 'tel:' + b_url.replace(' ', '').replace('-', '')
b['url'] = b_url
elif b_type == 'pdf':
pdf_file = request.files.get('block_pdf')
if pdf_file and pdf_file.filename and HF_TOKEN_WRITE:
if old_url and old_url.startswith('pdf_'):
try:
HfApi().delete_files(repo_id=REPO_ID, paths_in_repo=[f"pdfs/{old_url}"], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
try:
ext = os.path.splitext(pdf_file.filename)[1].lower()
pdf_filename = f"pdf_{uuid4().hex[:8]}{ext}"
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, pdf_filename)
pdf_file.save(temp_path)
HfApi().upload_file(path_or_fileobj=temp_path, path_in_repo=f"pdfs/{pdf_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
os.remove(temp_path)
b['url'] = pdf_filename
except Exception as e:
flash(f"Ошибка загрузки PDF: {e}", 'error')
b['url'] = old_url
else:
b['url'] = old_url
else:
b['url'] = ''
break
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Кнопка сохранена.", "success")
elif action == 'delete_block':
b_id = request.form.get('block_id')
block_to_del = next((b for b in blocks if b.get('id') == b_id), None)
if block_to_del:
if block_to_del.get('type') == 'pdf' and block_to_del.get('url') and block_to_del.get('url').startswith('pdf_') and HF_TOKEN_WRITE:
try:
HfApi().delete_files(repo_id=REPO_ID, paths_in_repo=[f"pdfs/{block_to_del.get('url')}"], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
blocks = [b for b in blocks if b.get('id') != b_id]
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Кнопка успешно удалена.", "success")
elif action == 'move_block_up':
b_id = request.form.get('block_id')
idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1)
if idx > 0:
blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx]
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Порядок изменён.", "success")
elif action == 'move_block_down':
b_id = request.form.get('block_id')
idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1)
if idx != -1 and idx < len(blocks) - 1:
blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx]
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Порядок изменён.", "success")
elif action == 'add_category':
category_name = request.form.get('category_name', '').strip()
if category_name and category_name not in categories:
categories.append(category_name)
data['categories'] = categories
save_env_data(env_id, data)
flash(f"Папка успешно создана.", 'success')
elif not category_name:
flash("Название папки не может быть пустым.", 'error')
else:
flash(f"Такая папка уже существует.", 'error')
elif action == 'delete_category':
category_to_delete = request.form.get('category_name')
if category_to_delete and category_to_delete in categories:
categories.remove(category_to_delete)
for product in products:
if product.get('category') == category_to_delete:
product['category'] = 'Без категории'
data['categories'] = categories
data['products'] = products
save_env_data(env_id, data)
flash(f"Папка удалена.", 'success')
elif action == 'update_settings':
settings['admin_password_enabled'] = 'admin_password_enabled' in request.form
settings['admin_password'] = request.form.get('admin_password', '').strip()
settings['vcard_firstname'] = request.form.get('vcard_firstname', '').strip()
settings['vcard_lastname'] = request.form.get('vcard_lastname', '').strip()
settings['vcard_job'] = request.form.get('vcard_job', '').strip()
settings['organization_name'] = request.form.get('organization_name', '').strip()
settings['about_text'] = request.form.get('about_text', '').strip()
settings['currency_code'] = request.form.get('currency_code', 'KGS')
settings['color_scheme'] = request.form.get('color_scheme', 'default')
settings['enable_cart'] = 'enable_cart' in request.form
settings['order_messenger'] = request.form.get('order_messenger', 'whatsapp')
settings['order_contact'] = request.form.get('order_contact', '').strip()
avatar_file = request.files.get('chat_avatar')
if avatar_file and avatar_file.filename and HF_TOKEN_WRITE:
try:
api = HfApi()
old_avatar = settings.get('chat_avatar')
if old_avatar:
try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
ext = os.path.splitext(avatar_file.filename)[1].lower()
avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}"
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, avatar_filename)
avatar_file.save(temp_path)
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
settings['chat_avatar'] = avatar_filename
os.remove(temp_path)
except Exception: pass
data['settings'] = settings
save_env_data(env_id, data)
flash("Настройки успешно сохранены.", 'success')
elif action == 'edit_product':
product_id = request.form.get('product_id')
product_data = next((p for p in products if p.get('product_id') == product_id), None)
if product_data:
try:
product_data['price'] = float(request.form.get('price', 0))
except ValueError:
product_data['price'] = 0.0
category = request.form.get('category')
product_data['category'] = category if category in categories else 'Без категории'
product_data['description'] = request.form.get('description', '').strip()
product_data['search_keywords'] = request.form.get('search_keywords', '').strip()
vars_str = request.form.get('variants', '')
product_data['variants'] = [v.strip() for v in vars_str.split(',') if v.strip()]
photo_files = request.files.getlist('photos')
valid_photos = [f for f in photo_files if f and f.filename]
if valid_photos and HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
new_filenames = []
for f in valid_photos:
try:
ext = os.path.splitext(f.filename)[1].lower()
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
photo_filename = f"photo_{uuid4().hex[:12]}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
f.save(temp_path)
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
os.remove(temp_path)
new_filenames.append(photo_filename)
except Exception: pass
if new_filenames:
if product_data.get('photos'):
try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
product_data['photos'] = new_filenames
data['products'] = products
save_env_data(env_id, data)
flash("Товар изменён.", 'success')
elif action == 'delete_product':
product_id = request.form.get('product_id')
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
if product_index != -1:
deleted_product = products.pop(product_index)
photos_to_delete = deleted_product.get('photos', [])
if photos_to_delete and HF_TOKEN_WRITE:
try:
api = HfApi()
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
data['products'] = products
save_env_data(env_id, data)
flash("Товар удалён.", 'success')
return redirect(url_for('admin', env_id=env_id, p=page, tab='archive' if show_archive else 'active'))
except Exception as e:
flash(f"Произошла ошибка: {e}", 'error')
return redirect(url_for('admin', env_id=env_id, p=page, tab='archive' if show_archive else 'active'))
if show_archive:
filtered_products = [p for p in reversed(products) if p.get('archived')]
else:
filtered_products = [p for p in reversed(products) if not p.get('archived')]
PER_PAGE = 20
total_items = len(filtered_products)
total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1
if page < 1: page = 1
if page > total_pages: page = total_pages
start_idx = (page - 1) * PER_PAGE
end_idx = start_idx + PER_PAGE
paginated_products = filtered_products[start_idx:end_idx]
display_categories = sorted(categories)
display_settings = settings
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
new_orders_count = sum(1 for o in data.get('orders', []) if not o.get('viewed', False))
return render_template_string(
ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, categories=display_categories,
settings=display_settings, blocks=blocks, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url,
currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, icons=ICONS, env_id=env_id, new_orders_count=new_orders_count, show_archive=show_archive
)
@app.route('//admin/orders')
def admin_orders(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
return redirect(url_for('admin_login', env_id=env_id))
orders = data.get('orders', [])
changed = False
for o in orders:
if not o.get('viewed', False):
o['viewed'] = True
changed = True
if changed:
save_env_data(env_id, data)
orders_sorted = sorted(orders, key=lambda x: x.get('date', ''), reverse=True)
return render_template_string(ADMIN_ORDERS_TEMPLATE, orders=orders_sorted, env_id=env_id, settings=settings, currency=settings.get('currency_code', 'KGS'), repo_id=REPO_ID)
@app.route('//admin/orders/delete/', methods=['POST'])
def delete_order(env_id, order_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
return redirect(url_for('admin_login', env_id=env_id))
orders = data.get('orders', [])
data['orders'] = [o for o in orders if o.get('id') != order_id]
save_env_data(env_id, data)
flash("Заказ успешно удален из истории.", "success")
return redirect(url_for('admin_orders', env_id=env_id))
if __name__ == '__main__':
download_db_from_hf()
load_data()
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
port = int(os.environ.get('PORT', 7860))
app.run(debug=False, host='0.0.0.0', port=port)