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/wizitowka" 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 = { 'PLN': 'Polski Złoty', 'EUR': 'Euro', 'USD': 'US Dollar', 'UAH': 'Українська гривня' } LANGUAGES = { 'pl': 'Polski', 'uk': 'Українська', 'zh': '中文', 'en': 'English', 'tr': 'Türkçe' } TRANSLATIONS = { 'pl': { 'login': 'Logowanie', 'admin_panel': 'Panel Administratora', 'enter_pwd': 'Wprowadź hasło', 'sign_in': 'Zaloguj się', 'card_mgmt': 'Zarządzanie wizytówkami', 'create_card': 'Utwórz nową wizytówkę', 'search_id_name': 'Szukaj po ID lub nazwie...', 'existing_cards': 'Istniejące wizytówki', 'settings': 'Ustawienia', 'view': 'Zobacz', 'delete': 'Usuń', 'enable_pwd': 'Włącz hasło', 'pwd': 'Hasło', 'save': 'Zapisz', 'no_cards': 'Brak wizytówek.', 'save_contacts': 'Zapisz do kontaktów', 'search_placeholder': 'Szukaj...', 'all': 'Wszystkie', 'min': 'Min', 'max': 'Max', 'apply_filter': 'Zastosuj', 'reset_all': 'Zresetuj', 'price_from': 'Cena od:', 'price_to': 'Cena do:', 'my_qr': 'Mój kod QR', 'scan_qr': 'Zeskanuj, aby otworzyć wizytówkę', 'cart': 'Koszyk', 'clear': 'Wyczyść', 'total': 'Suma:', 'checkout': 'Kasa', 'your_name': 'Twoje Imię', 'your_phone': 'Twój Telefon', 'confirm_order': 'Potwierdź zamówienie', 'cart_empty': 'Twój koszyk jest pusty', 'variant': 'Wariant:', 'add_to_cart': 'Do koszyka', 'site_settings': 'Ustawienia strony', 'orders': 'Zamówienia', 'save_qr': 'Zapisz QR', 'logout': 'Wyloguj', 'profile_app': 'Profil i Wygląd', 'first_name': 'Imię:', 'last_name': 'Nazwisko:', 'profession': 'Zawód / Stanowisko:', 'org_name': 'Nazwa organizacji:', 'about_me': 'Krótko o mnie (opis):', 'avatar': 'Twoje zdjęcie (Awatar):', 'current': 'Obecne:', 'design': 'Projekt:', 'currency': 'Waluta na stronie:', 'order_recv': 'Odbiór zamówień ze strony', 'enable_ord': 'Włącz zamawianie towarów', 'where_recv': 'Gdzie odbierać zamówienia:', 'to_wa': 'Na WhatsApp', 'to_tg': 'Na Telegram', 'no_recv': 'Nie przyjmuj', 'your_num': 'Twój numer lub login:', 'pwd_login': 'Hasło logowania tutaj', 'enable_pwd_prompt': 'Włącz zapytanie o hasło', 'save_settings': 'Zapisz ustawienia', 'btn_links': 'Przyciski i Linki', 'create_btn': 'Utwórz nowy przycisk', 'btn_type': 'Typ przycisku:', 'reg_link': 'Zwykły link/numer', 'txt_desc': 'Tekst (opis)', 'open_pdf': 'Otwórz PDF', 'btn_text': 'Napis na przycisku:', 'btn_icon': 'Ikona:', 'no_icon': '-- Brak ikony --', 'link_phone': 'Link lub telefon (z +):', 'sel_pdf': 'Wybierz PDF:', 'msg_text': 'Wiadomość:', 'add_btn': 'Dodaj przycisk', 'edit': 'Edytuj', 'up': 'Wyżej', 'down': 'Niżej', 'save_changes': 'Zapisz zmiany', 'folders': 'Foldery (do grupowania)', 'create_folder': 'Utwórz nowy folder', 'new_folder_name': 'Nazwa folderu:', 'del_folder': 'Usuń folder', 'no_folders': 'Brak folderów.', 'all_photos': 'Wszystkie zdjęcia i produkty', 'upload_multi': 'Prześlij wiele zdjęć na stronę', 'folder_to_add': 'Folder docelowy:', 'no_folder': 'Bez folderu', 'uncat': 'Bez kategorii', 'sel_photos': 'Wybierz zdjęcia:', 'publish': 'Opublikuj na stronie', 'active': 'Aktywne', 'archive': 'Archiwum', 'select': 'Wybierz', 'folder': 'Folder:', 'price': 'Cena:', 'desc': 'Opis:', 'variants': 'Warianty:', 'keywords': 'Słowa kluczowe:', 'to_archive': 'Do archiwum', 'restore': 'Przywróć', 'new_price': 'Nowa cena (bazowa):', 'move_folder': 'Przenieś do folderu:', 'desc_opt': 'Opis (opcjonalnie):', 'search_words': 'Słowa do wyszukiwania:', 'var_format': 'Warianty (Nazwa:Cena):', 'up_new_photos': 'Prześlij nowe zdjęcia zamiast starych:', 'back': 'Wstecz', 'forward': 'Dalej', 'group': 'Grupuj', 'order_inv': 'Faktura zamówienia', 'customer': 'Kupujący:', 'phone': 'Telefon:', 'seller': 'Sprzedawca:', 'pcs': 'szt.', 'send_seller': 'Wyślij do sprzedawcy', 'back_cat': 'Powrót do katalogu', 'order_hist': 'Historia zamówień', 'back_settings': 'Powrót do ustawień', 'invoice': 'Faktura', 'lang': 'Język', 'site_lang': 'Język strony:', 'no_photo': 'Brak zdjęcia', 'from': 'Od', 'nothing_found': 'Nic nie znaleziono.', 'price_filter': 'Filtr cenowy', 'order_proc': 'Przetwarzanie...', 'net_err': 'Błąd sieci', 'order_err': 'Błąd zamówienia', 'fill_name_phone': 'Wypełnij imię i telefon', 'cart_empty_alert': 'Koszyk jest pusty', 'clear_cart_conf': 'Czy na pewno chcesz wyczyścić koszyk?', 'upload_err': 'Błąd przesyłania.', 'uploading': 'Przesyłanie...', 'prep_files': 'Przygotowywanie plików...', 'pls_sel_photos': 'Proszę najpierw wybrać zdjęcia!', 'del_btn_conf': 'Usunąć ten przycisk?', 'restore_conf': 'Przywrócić wybrane towary z archiwum?', 'archive_conf': 'Przenieść wybrane towary do archiwum?', 'del_all_conf': 'Na pewno trwale usunąć wybrane towary?', 'del_item_conf': 'Trwale usunąć to zdjęcie/towar?', 'arch_item_conf': 'Wysłać towar do archiwum?', 'del_folder_conf': 'Usuń folder? Zdjęcia pozostaną.', 'del_card_conf': 'Na pewno usunąć wizytówkę?', 'viewed': 'Wyświetlone', 'new_ord': 'Nowe', 'saving_data': 'Proszę czekać...
Trwa zapisywanie danych', 'order_hash': 'Zamówienie #' }, 'uk': { 'login': 'Вхід', 'admin_panel': 'Адмін-панель', 'enter_pwd': 'Введіть пароль', 'sign_in': 'Увійти', 'card_mgmt': 'Управління візитками', 'create_card': 'Створити нову візитку', 'search_id_name': 'Пошук за ID або назвою...', 'existing_cards': 'Існуючі візитки', 'settings': 'Налаштування', 'view': 'Переглянути', 'delete': 'Видалити', 'enable_pwd': 'Увімкнути пароль', 'pwd': 'Пароль', 'save': 'Зберегти', 'no_cards': 'Немає візиток.', 'save_contacts': 'Зберегти в контакти', 'search_placeholder': 'Пошук...', 'all': 'Всі', 'min': 'Мін', 'max': 'Макс', 'apply_filter': 'Застосувати', 'reset_all': 'Скинути', 'price_from': 'Ціна від:', 'price_to': 'Ціна до:', 'my_qr': 'Мій QR-код', 'scan_qr': 'Відскануйте, щоб відкрити візитку', 'cart': 'Кошик', 'clear': 'Очистити', 'total': 'Разом:', 'checkout': 'Оформлення', 'your_name': 'Ваше Ім\'я', 'your_phone': 'Ваш Телефон', 'confirm_order': 'Підтвердити замовлення', 'cart_empty': 'Кошик порожній', 'variant': 'Варіант:', 'add_to_cart': 'В замовлення', 'site_settings': 'Налаштування сайту', 'orders': 'Замовлення', 'save_qr': 'Зберегти QR', 'logout': 'Вийти', 'profile_app': 'Профіль та Вигляд', 'first_name': 'Ім\'я:', 'last_name': 'Прізвище:', 'profession': 'Професія / Посада:', 'org_name': 'Назва організації:', 'about_me': 'Коротко про себе:', 'avatar': 'Фото (Аватар):', 'current': 'Поточна:', 'design': 'Дизайн:', 'currency': 'Валюта на сайті:', 'order_recv': 'Прийом замовлень', 'enable_ord': 'Увімкнути замовлення', 'where_recv': 'Куди отримувати:', 'to_wa': 'На WhatsApp', 'to_tg': 'На Telegram', 'no_recv': 'Не приймати', 'your_num': 'Ваш номер/логін:', 'pwd_login': 'Пароль для входу', 'enable_pwd_prompt': 'Запит пароля', 'save_settings': 'Зберегти налаштування', 'btn_links': 'Кнопки та Посилання', 'create_btn': 'Створити кнопку', 'btn_type': 'Тип кнопки:', 'reg_link': 'Посилання/номер', 'txt_desc': 'Текст (опис)', 'open_pdf': 'Відкрити PDF', 'btn_text': 'Напис на кнопці:', 'btn_icon': 'Іконка:', 'no_icon': '-- Без іконки --', 'link_phone': 'Посилання/телефон:', 'sel_pdf': 'Вибрати PDF:', 'msg_text': 'Текст:', 'add_btn': 'Додати кнопку', 'edit': 'Редагувати', 'up': 'Вище', 'down': 'Нижче', 'save_changes': 'Зберегти зміни', 'folders': 'Папки', 'create_folder': 'Створити папку', 'new_folder_name': 'Назва папки:', 'del_folder': 'Видалити папку', 'no_folders': 'Немає папок.', 'all_photos': 'Всі фото та товари', 'upload_multi': 'Завантажити фото', 'folder_to_add': 'Папка:', 'no_folder': 'Без папки', 'uncat': 'Без категорії', 'sel_photos': 'Вибрати фото:', 'publish': 'Опублікувати', 'active': 'Активні', 'archive': 'Архів', 'select': 'Відмітити', 'folder': 'Папка:', 'price': 'Ціна:', 'desc': 'Опис:', 'variants': 'Варіанти:', 'keywords': 'Ключові слова:', 'to_archive': 'В архів', 'restore': 'Відновити', 'new_price': 'Нова ціна:', 'move_folder': 'Перенести:', 'desc_opt': 'Опис (опціонально):', 'search_words': 'Слова для пошуку:', 'var_format': 'Варіанти (Назва:Ціна):', 'up_new_photos': 'Нові фото:', 'back': 'Назад', 'forward': 'Вперед', 'group': 'Згрупувати', 'order_inv': 'Накладна', 'customer': 'Покупець:', 'phone': 'Телефон:', 'seller': 'Продавець:', 'pcs': 'шт.', 'send_seller': 'Відправити продавцю', 'back_cat': 'В каталог', 'order_hist': 'Історія замовлень', 'back_settings': 'До налаштувань', 'invoice': 'Накладна', 'lang': 'Мова', 'site_lang': 'Мова сайту:', 'no_photo': 'Немає фото', 'from': 'Від', 'nothing_found': 'Нічого не знайдено.', 'price_filter': 'Фільтр за ціною', 'order_proc': 'Оформлення...', 'net_err': 'Помилка мережі', 'order_err': 'Помилка замовлення', 'fill_name_phone': 'Введіть ім\'я та телефон', 'cart_empty_alert': 'Кошик порожній', 'clear_cart_conf': 'Очистити кошик?', 'upload_err': 'Помилка завантаження.', 'uploading': 'Завантаження...', 'prep_files': 'Підготовка...', 'pls_sel_photos': 'Виберіть фото!', 'del_btn_conf': 'Видалити кнопку?', 'restore_conf': 'Відновити товари?', 'archive_conf': 'В архів?', 'del_all_conf': 'Видалити назавжди?', 'del_item_conf': 'Видалити товар?', 'arch_item_conf': 'В архів?', 'del_folder_conf': 'Видалити папку? Фото залишаться.', 'del_card_conf': 'Видалити візитку?', 'viewed': 'Переглянуто', 'new_ord': 'Нові', 'saving_data': 'Будь ласка, зачекайте...
Йде збереження даних', 'order_hash': 'Замовлення #' }, 'zh': { 'login': '登录', 'admin_panel': '管理面板', 'enter_pwd': '输入密码', 'sign_in': '登录', 'card_mgmt': '名片管理', 'create_card': '创建名片', 'search_id_name': '搜索ID或名称...', 'existing_cards': '现有名片', 'settings': '设置', 'view': '查看', 'delete': '删除', 'enable_pwd': '启用密码', 'pwd': '密码', 'save': '保存', 'no_cards': '没有名片。', 'save_contacts': '保存联系人', 'search_placeholder': '搜索...', 'all': '全部', 'min': '最低', 'max': '最高', 'apply_filter': '应用', 'reset_all': '重置', 'price_from': '价格从:', 'price_to': '价格至:', 'my_qr': '我的二维码', 'scan_qr': '扫码打开名片', 'cart': '购物车', 'clear': '清空', 'total': '总计:', 'checkout': '结账', 'your_name': '您的姓名', 'your_phone': '您的电话', 'confirm_order': '确认订单', 'cart_empty': '购物车为空', 'variant': '款式:', 'add_to_cart': '加入购物车', 'site_settings': '网站设置', 'orders': '订单', 'save_qr': '保存二维码', 'logout': '登出', 'profile_app': '个人资料和外观', 'first_name': '名:', 'last_name': '姓:', 'profession': '职业 / 职位:', 'org_name': '组织名称:', 'about_me': '关于我:', 'avatar': '头像:', 'current': '当前:', 'design': '设计:', 'currency': '货币:', 'order_recv': '订单接收', 'enable_ord': '启用下单', 'where_recv': '接收位置:', 'to_wa': 'WhatsApp', 'to_tg': 'Telegram', 'no_recv': '不接收', 'your_num': '您的号码/登录名:', 'pwd_login': '登录密码', 'enable_pwd_prompt': '启用密码提示', 'save_settings': '保存设置', 'btn_links': '按钮和链接', 'create_btn': '创建按钮', 'btn_type': '类型:', 'reg_link': '链接/号码', 'txt_desc': '文本(描述)', 'open_pdf': '打开PDF', 'btn_text': '按钮文字:', 'btn_icon': '图标:', 'no_icon': '-- 无 --', 'link_phone': '链接/电话:', 'sel_pdf': '选择PDF:', 'msg_text': '消息:', 'add_btn': '添加按钮', 'edit': '编辑', 'up': '向上', 'down': '向下', 'save_changes': '保存更改', 'folders': '文件夹', 'create_folder': '创建文件夹', 'new_folder_name': '文件夹名称:', 'del_folder': '删除文件夹', 'no_folders': '无文件夹。', 'all_photos': '所有照片和产品', 'upload_multi': '上传多张照片', 'folder_to_add': '目标文件夹:', 'no_folder': '无', 'uncat': '未分类', 'sel_photos': '选择照片:', 'publish': '在网站上发布', 'active': '活跃', 'archive': '归档', 'select': '选择', 'folder': '文件夹:', 'price': '价格:', 'desc': '描述:', 'variants': '款式:', 'keywords': '关键字:', 'to_archive': '归档', 'restore': '恢复', 'new_price': '新价格:', 'move_folder': '移动到:', 'desc_opt': '描述(可选):', 'search_words': '搜索词:', 'var_format': '款式(名:价):', 'up_new_photos': '上传新照片:', 'back': '返回', 'forward': '前进', 'group': '分组', 'order_inv': '订单发票', 'customer': '客户:', 'phone': '电话:', 'seller': '卖家:', 'pcs': '件', 'send_seller': '发送给卖家', 'back_cat': '返回目录', 'order_hist': '订单历史', 'back_settings': '返回设置', 'invoice': '发票', 'lang': '语言', 'site_lang': '网站语言:', 'no_photo': '无照片', 'from': '起', 'nothing_found': '未找到。', 'price_filter': '价格筛选', 'order_proc': '处理中...', 'net_err': '网络错误', 'order_err': '订单错误', 'fill_name_phone': '请填写姓名电话', 'cart_empty_alert': '购物车空', 'clear_cart_conf': '清空购物车?', 'upload_err': '上传错误', 'uploading': '上传中...', 'prep_files': '准备文件中...', 'pls_sel_photos': '请先选择照片!', 'del_btn_conf': '删除此按钮?', 'restore_conf': '恢复这些项目?', 'archive_conf': '归档这些项目?', 'del_all_conf': '永久删除?', 'del_item_conf': '删除此项目?', 'arch_item_conf': '归档此项目?', 'del_folder_conf': '删除文件夹?照片将保留。', 'del_card_conf': '删除名片?', 'viewed': '已读', 'new_ord': '新', 'saving_data': '请稍候...
正在保存数据', 'order_hash': '订单 #' }, 'en': { 'login': 'Login', 'admin_panel': 'Admin Panel', 'enter_pwd': 'Enter password', 'sign_in': 'Sign in', 'card_mgmt': 'Card Management', 'create_card': 'Create new card', 'search_id_name': 'Search by ID or Name...', 'existing_cards': 'Existing cards', 'settings': 'Settings', 'view': 'View', 'delete': 'Delete', 'enable_pwd': 'Enable password', 'pwd': 'Password', 'save': 'Save', 'no_cards': 'No cards created yet.', 'save_contacts': 'Save to contacts', 'search_placeholder': 'Search...', 'all': 'All', 'min': 'Min', 'max': 'Max', 'apply_filter': 'Apply', 'reset_all': 'Reset', 'price_from': 'Price from:', 'price_to': 'Price to:', 'my_qr': 'My QR Code', 'scan_qr': 'Scan to open card', 'cart': 'Cart', 'clear': 'Clear', 'total': 'Total:', 'checkout': 'Checkout', 'your_name': 'Your Name', 'your_phone': 'Your Phone', 'confirm_order': 'Confirm order', 'cart_empty': 'Your cart is empty', 'variant': 'Variant:', 'add_to_cart': 'Add to cart', 'site_settings': 'Site Settings', 'orders': 'Orders', 'save_qr': 'Save QR', 'logout': 'Logout', 'profile_app': 'Profile & Appearance', 'first_name': 'First Name:', 'last_name': 'Last Name:', 'profession': 'Profession / Title:', 'org_name': 'Organization Name:', 'about_me': 'About me:', 'avatar': 'Avatar:', 'current': 'Current:', 'design': 'Design:', 'currency': 'Site Currency:', 'order_recv': 'Order Receiving', 'enable_ord': 'Enable ordering', 'where_recv': 'Where to receive:', 'to_wa': 'To WhatsApp', 'to_tg': 'To Telegram', 'no_recv': 'Do not receive', 'your_num': 'Your number/login:', 'pwd_login': 'Login password here', 'enable_pwd_prompt': 'Enable password prompt', 'save_settings': 'Save settings', 'btn_links': 'Buttons & Links', 'create_btn': 'Create button', 'btn_type': 'Button type:', 'reg_link': 'Regular link/number', 'txt_desc': 'Text (description)', 'open_pdf': 'Open PDF', 'btn_text': 'Button text (title):', 'btn_icon': 'Icon:', 'no_icon': '-- No icon --', 'link_phone': 'Link/phone (with +):', 'sel_pdf': 'Select PDF:', 'msg_text': 'Message text:', 'add_btn': 'Add button', 'edit': 'Edit', 'up': 'Up', 'down': 'Down', 'save_changes': 'Save changes', 'folders': 'Folders', 'create_folder': 'Create folder', 'new_folder_name': 'Folder name:', 'del_folder': 'Delete folder', 'no_folders': 'No folders.', 'all_photos': 'All photos & products', 'upload_multi': 'Upload multiple photos', 'folder_to_add': 'Target folder:', 'no_folder': 'No folder', 'uncat': 'Uncategorized', 'sel_photos': 'Select photos:', 'publish': 'Publish', 'active': 'Active', 'archive': 'Archive', 'select': 'Select', 'folder': 'Folder:', 'price': 'Price:', 'desc': 'Description:', 'variants': 'Variants:', 'keywords': 'Keywords:', 'to_archive': 'To archive', 'restore': 'Restore', 'new_price': 'New price:', 'move_folder': 'Move to:', 'desc_opt': 'Description (opt):', 'search_words': 'Search words:', 'var_format': 'Variants (Name:Price):', 'up_new_photos': 'Upload new photos:', 'back': 'Back', 'forward': 'Forward', 'group': 'Group', 'order_inv': 'Order Invoice', 'customer': 'Customer:', 'phone': 'Phone:', 'seller': 'Seller:', 'pcs': 'pcs.', 'send_seller': 'Send to seller', 'back_cat': 'Back to catalog', 'order_hist': 'Order history', 'back_settings': 'Back to settings', 'invoice': 'Invoice', 'lang': 'Language', 'site_lang': 'Site Language:', 'no_photo': 'No photo', 'from': 'From', 'nothing_found': 'Nothing found.', 'price_filter': 'Price filter', 'order_proc': 'Processing...', 'net_err': 'Network error', 'order_err': 'Order error', 'fill_name_phone': 'Please fill name and phone', 'cart_empty_alert': 'Cart is empty', 'clear_cart_conf': 'Clear cart?', 'upload_err': 'Upload error.', 'uploading': 'Uploading...', 'prep_files': 'Preparing files...', 'pls_sel_photos': 'Please select photos!', 'del_btn_conf': 'Delete this button?', 'restore_conf': 'Restore these items?', 'archive_conf': 'Archive these items?', 'del_all_conf': 'Delete permanently?', 'del_item_conf': 'Delete this item?', 'arch_item_conf': 'Archive this item?', 'del_folder_conf': 'Delete folder? Photos remain.', 'del_card_conf': 'Delete card?', 'viewed': 'Viewed', 'new_ord': 'New', 'saving_data': 'Please wait...
Saving data', 'order_hash': 'Order #' }, 'tr': { 'login': 'Giriş', 'admin_panel': 'Yönetici Paneli', 'enter_pwd': 'Şifreyi girin', 'sign_in': 'Giriş yap', 'card_mgmt': 'Kart Yönetimi', 'create_card': 'Yeni kart oluştur', 'search_id_name': 'ID veya İsme göre ara...', 'existing_cards': 'Mevcut kartlar', 'settings': 'Ayarlar', 'view': 'Görüntüle', 'delete': 'Sil', 'enable_pwd': 'Şifreyi etkinleştir', 'pwd': 'Şifre', 'save': 'Kaydet', 'no_cards': 'Henüz kart oluşturulmadı.', 'save_contacts': 'Kişilere kaydet', 'search_placeholder': 'Ara...', 'all': 'Tümü', 'min': 'Min', 'max': 'Maks', 'apply_filter': 'Uygula', 'reset_all': 'Sıfırla', 'price_from': 'Başlangıç fiyatı:', 'price_to': 'Bitiş fiyatı:', 'my_qr': 'QR Kodum', 'scan_qr': 'Kartı açmak için tarayın', 'cart': 'Sepet', 'clear': 'Temizle', 'total': 'Toplam:', 'checkout': 'Ödeme', 'your_name': 'Adınız', 'your_phone': 'Telefonunuz', 'confirm_order': 'Siparişi onayla', 'cart_empty': 'Sepetiniz boş', 'variant': 'Varyant:', 'add_to_cart': 'Sepete ekle', 'site_settings': 'Site Ayarları', 'orders': 'Siparişler', 'save_qr': 'QR Kaydet', 'logout': 'Çıkış', 'profile_app': 'Profil ve Görünüm', 'first_name': 'Ad:', 'last_name': 'Soyad:', 'profession': 'Meslek / Ünvan:', 'org_name': 'Kurum Adı:', 'about_me': 'Hakkımda:', 'avatar': 'Avatar:', 'current': 'Mevcut:', 'design': 'Tasarım:', 'currency': 'Site Para Birimi:', 'order_recv': 'Sipariş Alımı', 'enable_ord': 'Siparişi etkinleştir', 'where_recv': 'Nereye alınacak:', 'to_wa': 'WhatsApp', 'to_tg': 'Telegram', 'no_recv': 'Alma', 'your_num': 'Numaranız/Kullanıcı adınız:', 'pwd_login': 'Giriş şifresi', 'enable_pwd_prompt': 'Şifre sormayı etkinleştir', 'save_settings': 'Ayarları kaydet', 'btn_links': 'Butonlar ve Bağlantılar', 'create_btn': 'Buton oluştur', 'btn_type': 'Buton tipi:', 'reg_link': 'Normal link/numara', 'txt_desc': 'Metin (açıklama)', 'open_pdf': 'PDF Aç', 'btn_text': 'Buton metni (başlık):', 'btn_icon': 'İkon:', 'no_icon': '-- İkon yok --', 'link_phone': 'Link/telefon (+ ile):', 'sel_pdf': 'PDF seç:', 'msg_text': 'Mesaj metni:', 'add_btn': 'Buton ekle', 'edit': 'Düzenle', 'up': 'Yukarı', 'down': 'Aşağı', 'save_changes': 'Değişiklikleri kaydet', 'folders': 'Klasörler', 'create_folder': 'Klasör oluştur', 'new_folder_name': 'Klasör adı:', 'del_folder': 'Klasörü sil', 'no_folders': 'Klasör yok.', 'all_photos': 'Tüm fotoğraflar ve ürünler', 'upload_multi': 'Çoklu fotoğraf yükle', 'folder_to_add': 'Hedef klasör:', 'no_folder': 'Klasörsüz', 'uncat': 'Kategorisiz', 'sel_photos': 'Fotoğrafları seç:', 'publish': 'Yayınla', 'active': 'Aktif', 'archive': 'Arşiv', 'select': 'Seç', 'folder': 'Klasör:', 'price': 'Fiyat:', 'desc': 'Açıklama:', 'variants': 'Varyantlar:', 'keywords': 'Anahtar kelimeler:', 'to_archive': 'Arşive gönder', 'restore': 'Geri yükle', 'new_price': 'Yeni fiyat:', 'move_folder': 'Şuraya taşı:', 'desc_opt': 'Açıklama (opsiyonel):', 'search_words': 'Arama kelimeleri:', 'var_format': 'Varyantlar (Ad:Fiyat):', 'up_new_photos': 'Yeni fotoğraflar yükle:', 'back': 'Geri', 'forward': 'İleri', 'group': 'Grupla', 'order_inv': 'Sipariş Faturası', 'customer': 'Müşteri:', 'phone': 'Telefon:', 'seller': 'Satıcı:', 'pcs': 'adet', 'send_seller': 'Satıcıya gönder', 'back_cat': 'Kataloğa dön', 'order_hist': 'Sipariş geçmişi', 'back_settings': 'Ayarlara dön', 'invoice': 'Fatura', 'lang': 'Dil', 'site_lang': 'Site Dili:', 'no_photo': 'Fotoğraf yok', 'from': 'Başlangıç', 'nothing_found': 'Hiçbir şey bulunamadı.', 'price_filter': 'Fiyat filtresi', 'order_proc': 'İşleniyor...', 'net_err': 'Ağ hatası', 'order_err': 'Sipariş hatası', 'fill_name_phone': 'Lütfen adınızı ve telefonunuzu doldurun', 'cart_empty_alert': 'Sepet boş', 'clear_cart_conf': 'Sepeti temizlemek istiyor musunuz?', 'upload_err': 'Yükleme hatası.', 'uploading': 'Yükleniyor...', 'prep_files': 'Dosyalar hazırlanıyor...', 'pls_sel_photos': 'Lütfen önce fotoğrafları seçin!', 'del_btn_conf': 'Bu butonu silmek istiyor musunuz?', 'restore_conf': 'Bu öğeleri geri yüklemek istiyor musunuz?', 'archive_conf': 'Bu öğeleri arşive taşımak istiyor musunuz?', 'del_all_conf': 'Kalıcı olarak silinsin mi?', 'del_item_conf': 'Bu öğeyi silmek istiyor musunuz?', 'arch_item_conf': 'Bu öğeyi arşive taşımak istiyor musunuz?', 'del_folder_conf': 'Klasör silinsin mi? Fotoğraflar kalacaktır.', 'del_card_conf': 'Kart silinsin mi?', 'viewed': 'Görüldü', 'new_ord': 'Yeni', 'saving_data': 'Lütfen bekleyin...
Veriler kaydediliyor', 'order_hash': 'Sipariş #' } } def get_t(key, lang='pl'): return TRANSLATIONS.get(lang, TRANSLATIONS['en']).get(key, key) app.jinja_env.globals.update(t=get_t) 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": "Name", "vcard_lastname": "Lastname", "vcard_job": "Specialist", "organization_name": "My Company", "currency_code": "PLN", "site_language": "pl", "chat_avatar": None, "color_scheme": "default", "admin_password_enabled": False, "admin_password": "", "categories_as_lines": False, "about_text": "Hello! This is my vCard.", "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 = ''' Online VCard Platform ''' LOGIN_TEMPLATE = ''' {{ t('admin_panel', lang) }}

{{ t('login', lang) }}

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
''' ADMHOSTO_TEMPLATE = ''' {{ t('admin_panel', lang) }}

{{ t('card_mgmt', lang) }}

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

{{ t('existing_cards', lang) }}

{% if environments %} {% else %}

{{ t('no_cards', lang) }}

{% endif %}
''' CATALOG_TEMPLATE = ''' {{ settings.vcard_firstname }} {{ settings.vcard_lastname }}
Avatar
{{ settings.vcard_firstname }} {{ settings.vcard_lastname }}
{% if settings.vcard_job %}
{{ settings.vcard_job }}
{% endif %} {% if settings.organization_name %}
{{ settings.organization_name }}
{% endif %} {% if settings.about_text %}
{{ settings.about_text|replace('\\n', '
')|safe }}
{% endif %}
{{ t('save_contacts', lang) }} {% if blocks %}
{% for block in blocks %} {% if block.type == 'link' %} {% 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 }} {% elif block.type == 'text' %}
{% if block.title %}

{{ block.title }}

{% endif %}

{{ block.content|replace('\\n', '
')|safe }}

{% elif block.type == 'pdf' %} {% endif %} {% endfor %}
{% endif %} {% if products_json != '[]' %}
{% endif %}
''' ADMIN_TEMPLATE = ''' {{ t('site_settings', lang) }} - {{ settings.organization_name }}

{{ t('saving_data', lang)|safe }}

Logo

{{ t('site_settings', lang) }}

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
{{ t('profile_app', lang) }}
{% if settings.chat_avatar %}

{{ t('current', lang) }}

{% endif %}

{{ t('order_recv', lang) }}

{{ t('pwd_login', lang) }}

{{ t('btn_links', lang) }}

{{ t('create_btn', lang) }}
{% 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' %}Link{% elif block.type == 'pdf' %}PDF{% else %}Text{% endif %}) {% if block.type == 'link' %}
{{ block.url }}{% endif %}
{% endfor %} {% else %}

--

{% endif %}

{{ t('folders', lang) }}

{{ t('create_folder', lang) }}
{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}

{{ t('no_folders', lang) }}

{% endif %}

{{ t('all_photos', lang) }}

{{ t('upload_multi', lang) }}
{{ t('active', lang) }} {{ t('archive', lang) }}
{% if paginated_products %}
{% for product in paginated_products %}
{{ t('select', lang) }}
{% if product.get('photos') %} {% for p in product['photos'] %} Photo {% endfor %} {% else %} No photo {% endif %}

{{ t('folder', lang) }} {{ product.get('category', t('no_folder', lang)) if product.get('category') != 'Без категории' else t('uncat', lang) }}

{{ t('price', lang) }} {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}0{% endif %}

{% if product.get('description') %}

{{ t('desc', lang) }} {{ product.get('description') }}

{% endif %} {% if product.get('variants') %}

{{ t('variants', lang) }} {{ ', '.join(product.get('variants')) }}

{% endif %} {% if product.get('search_keywords') %}

{{ t('keywords', lang) }} {{ product.get('search_keywords') }}

{% endif %}
{% if show_archive %}
{% else %}
{% endif %}

{{ t('edit', lang) }}

{% endfor %}
{% if total_pages > 1 %} {% endif %} {% else %}

{{ t('nothing_found', lang) }}

{% endif %}
''' ORDER_TEMPLATE = ''' {{ t('order_inv', lang) }} #{{ order['id'] }}

{{ t('order_inv', lang) }}

#{{ order['id'] }} {{ t('from', lang).lower() }} {{ order['date'] }}

{{ t('customer', lang) }} {{ order['customer_name'] }}

{{ t('phone', lang) }} {{ order['customer_phone'] }}

{{ t('seller', lang) }} {{ settings['organization_name'] }}

{% for cid, item in order['items'].items() %}
{% if item['photo'] %} Photo {% else %} No photo {% endif %}
{{ item['qty'] }} {{ t('pcs', lang) }} × {{ "%.2f"|format(item['price']|float) }} {{ currency }}
{% if item.get('variant') %}
{{ t('variant', lang) }} {{ item['variant'] }}
{% endif %}
{{ "%.2f"|format(item['qty']|int * item['price']|float) }} {{ currency }}
{% endfor %}
{{ t('total', lang) }} {{ "%.2f"|format(order['total']|float) }} {{ currency }}
{% if send_link != "#" %} {{ t('send_seller', lang) }} ({{ messenger_name }}) {% endif %} {{ t('back_cat', lang) }}
''' ADMIN_ORDERS_TEMPLATE = ''' {{ t('order_hist', lang) }} - {{ settings.organization_name }}

{{ t('order_hist', lang) }}

{{ t('back_settings', lang) }}
{% if orders %} {% for order in orders %}
{{ t('order_hash', lang) }}{{ order['id'] }} {{ order['date'] }}
{{ t('invoice', lang) }}

{{ t('customer', lang) }} {{ order['customer_name'] }}

{{ t('phone', lang) }} {{ order['customer_phone'] }}

{% for cid, item in order['items'].items() %}
{% if item['photo'] %} {% else %}
{{ t('no_photo', lang) }}
{% endif %} {{ item['qty'] }} {{ t('pcs', lang) }} {% if item.get('variant') %}
{{ t('variant', lang) }} {{ item['variant'] }}{% endif %}
{{ "%.2f"|format(item['price']|float * item['qty']|int) }} {{ currency }}
{% endfor %}
{{ t('total', lang) }} {{ "%.2f"|format(order['total']|float) }} {{ currency }}
{% endfor %} {% else %}

{{ t('nothing_found', lang) }}

{% 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, lang='pl') @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": "Name", "vcard_lastname": "Lastname", "vcard_job": "Specialist", "organization_name": "My Company", "currency_code": "PLN", "site_language": "pl", "chat_avatar": None, "color_scheme": "default", "admin_password_enabled": False, "admin_password": "", "categories_as_lines": False, "about_text": "Hello! This is my vCard.", "enable_cart": False, "order_messenger": "whatsapp", "order_contact": "" } } save_data(all_data) flash(f'Nowa wizytówka o ID {new_id} pomyślnie utworzona.', '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'Hasło zaktualizowane.', 'success') else: flash(f'Nie znaleziono.', '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'Pomyślnie usunięto.', 'success') else: flash(f'Nie znaleziono.', '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', {}) lang = settings.get('site_language', 'pl') 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(get_t('order_err', lang), 'error') return render_template_string(LOGIN_TEMPLATE, env_id=env_id, lang=lang) @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',[]) req_lang = request.args.get('lang') if req_lang and req_lang in LANGUAGES: session[f'lang_{env_id}'] = req_lang lang = session.get(f'lang_{env_id}', settings.get('site_language', 'pl')) active_products = [p for p in all_products if not p.get('archived', False)] product_categories = set(p.get('category', get_t('uncat', lang)) 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', get_t('uncat', lang))].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', 'PLN'), settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks, lang=lang, languages=LANGUAGES ) @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": "Cart is empty"}) 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', []) req_lang = request.args.get('lang') if req_lang and req_lang in LANGUAGES: session[f'lang_{env_id}'] = req_lang lang = session.get(f'lang_{env_id}', settings.get('site_language', 'pl')) order = next((o for o in orders if o.get('id') == order_id), None) if not order: return "Not found", 404 currency = settings.get('currency_code', 'PLN') text = f"New order #{order['id']} for {order['total']} {currency}.\nLink: {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 = "None" 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, lang=lang ) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) req_lang = request.args.get('lang') if req_lang and req_lang in LANGUAGES: session[f'lang_{env_id}'] = req_lang lang = session.get(f'lang_{env_id}', settings.get('site_language', 'pl')) 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', get_t('uncat', lang)) 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", '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", '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", '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", '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", '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"Error 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", "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"Error 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", "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", "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", "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", "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", 'success') elif not category_name: flash("Error empty", 'error') else: flash(f"Exists", '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'] = get_t('uncat', lang) data['categories'] = categories data['products'] = products save_env_data(env_id, data) flash(f"Success", '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['site_language'] = request.form.get('site_language', 'pl') 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', 'PLN') 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", '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 get_t('uncat', lang) 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", '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", '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"Error: {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', 'PLN'), 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, lang=lang, languages=LANGUAGES ) @app.route('//admin/orders') def admin_orders(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) req_lang = request.args.get('lang') if req_lang and req_lang in LANGUAGES: session[f'lang_{env_id}'] = req_lang lang = session.get(f'lang_{env_id}', settings.get('site_language', 'pl')) 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', 'PLN'), repo_id=REPO_ID, lang=lang) @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", "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)