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 PIL import Image
import google.generativeai as genai
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/qrex"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
ALMATY_TZ = timezone(timedelta(hours=6))
db_lock = threading.RLock()
TRANSLATIONS = {
'Zaloguj się do Panelu': {'en': 'Login to Admin Panel', 'de': 'Anmeldung zum Admin-Panel'},
'Zaloguj': {'en': 'Login', 'de': 'Anmelden'},
'Wprowadź hasło': {'en': 'Enter password', 'de': 'Passwort eingeben'},
'Główny Panel Administracyjny': {'en': 'Main Admin Panel', 'de': 'Haupt-Admin-Panel'},
'Zarządzanie Wizytówkami': {'en': 'vCard Management', 'de': 'Visitenkartenverwaltung'},
'Utwórz nową wizytówkę': {'en': 'Create new vCard', 'de': 'Neue Visitenkarte erstellen'},
'Szukaj po ID lub Nazwie...': {'en': 'Search by ID or Name...', 'de': 'Suchen nach ID oder Name...'},
'Istniejące wizytówki': {'en': 'Existing vCards', 'de': 'Vorhandene Visitenkarten'},
'Ustawienia': {'en': 'Settings', 'de': 'Einstellungen'},
'Zobacz': {'en': 'View', 'de': 'Ansehen'},
'Włącz hasło admina': {'en': 'Enable admin pwd', 'de': 'Admin-Passwort aktivieren'},
'Hasło': {'en': 'Password', 'de': 'Passwort'},
'Zapisz': {'en': 'Save', 'de': 'Speichern'},
'Nie utworzono jeszcze żadnych wizytówek.': {'en': 'No vCards created yet.', 'de': 'Noch keine Visitenkarten erstellt.'},
'Zapisz w kontaktach': {'en': 'Save to contacts', 'de': 'In Kontakten speichern'},
'Katalog / Usługi': {'en': 'Catalog / Services', 'de': 'Katalog / Dienstleistungen'},
'Szukaj...': {'en': 'Search...', 'de': 'Suchen...'},
'Wszystkie': {'en': 'All', 'de': 'Alle'},
'Do koszyka': {'en': 'Add to cart', 'de': 'In den Warenkorb'},
'Mój kod QR': {'en': 'My QR code', 'de': 'Mein QR-Code'},
'Zeskanuj, aby otworzyć wizytówkę': {'en': 'Scan to open vCard', 'de': 'Scannen, um Visitenkarte zu öffnen'},
'Koszyk': {'en': 'Cart', 'de': 'Warenkorb'},
'Twoje Imię': {'en': 'Your Name', 'de': 'Ihr Name'},
'Twój Telefon': {'en': 'Your Phone', 'de': 'Ihre Telefonnummer'},
'Złóż zamówienie': {'en': 'Checkout', 'de': 'Kasse'},
'Razem:': {'en': 'Total:', 'de': 'Gesamt:'},
'Koszyk jest pusty': {'en': 'Cart is empty', 'de': 'Warenkorb ist leer'},
'Brak wyników.': {'en': 'Nothing found.', 'de': 'Nichts gefunden.'},
'Przetwarzanie...': {'en': 'Processing...', 'de': 'Verarbeitung...'},
'Błąd składania zamówienia': {'en': 'Checkout error', 'de': 'Bestellfehler'},
'Błąd sieci': {'en': 'Network error', 'de': 'Netzwerkfehler'},
'Wypełnij imię i telefon': {'en': 'Fill in name and phone', 'de': 'Bitte Name und Telefon ausfüllen'},
'Otwórz wizytówkę': {'en': 'Open vCard', 'de': 'Visitenkarte öffnen'},
'Historia zamówień': {'en': 'Order History', 'de': 'Bestellhistorie'},
'Kod QR': {'en': 'QR Code', 'de': 'QR-Code'},
'Wyloguj': {'en': 'Logout', 'de': 'Abmelden'},
'Profil i wygląd': {'en': 'Profile & Appearance', 'de': 'Profil & Aussehen'},
'Imię:': {'en': 'First Name:', 'de': 'Vorname:'},
'Nazwisko:': {'en': 'Last Name:', 'de': 'Nachname:'},
'Stanowisko / Specjalizacja:': {'en': 'Job Title:', 'de': 'Beruf / Spezialisierung:'},
'Nazwa organizacji:': {'en': 'Organization:', 'de': 'Organisation:'},
'O mnie / Status:': {'en': 'About / Status:', 'de': 'Über mich / Status:'},
'Awatar (Zdjęcie profilowe):': {'en': 'Avatar (Profile Photo):', 'de': 'Avatar (Profilbild):'},
'Schemat kolorów:': {'en': 'Color Scheme:', 'de': 'Farbschema:'},
'Waluta (dla produktów/usług):': {'en': 'Currency (for products):', 'de': 'Währung (für Produkte):'},
'Ustawienia koszyka i zamówień': {'en': 'Cart & Orders Settings', 'de': 'Warenkorb & Bestellungen'},
'Włącz koszyk (zakupy)': {'en': 'Enable cart (purchases)', 'de': 'Warenkorb aktivieren (Käufe)'},
'Komunikator dla zamówień:': {'en': 'Order messenger:', 'de': 'Bestell-Messenger:'},
'Nie przyjmuj zamówień': {'en': 'Do not accept orders', 'de': 'Keine Bestellungen annehmen'},
'Numer (WhatsApp) lub Username (Telegram):': {'en': 'Number (WhatsApp) / Username (Telegram):', 'de': 'Nummer (WhatsApp) / Benutzername (Telegram):'},
'Hasło dostępu (dla admina)': {'en': 'Access Password (for admin)', 'de': 'Zugangspasswort (für Admin)'},
'Wymagaj hasła': {'en': 'Require password', 'de': 'Passwort erforderlich'},
'Zapisz ustawienia': {'en': 'Save settings', 'de': 'Einstellungen speichern'},
'Przyciski i Linki': {'en': 'Buttons & Links', 'de': 'Schaltflächen & Links'},
'Dodaj przycisk': {'en': 'Add button', 'de': 'Schaltfläche hinzufügen'},
'Typ bloku:': {'en': 'Block type:', 'de': 'Blocktyp:'},
'Przycisk-link': {'en': 'Link button', 'de': 'Link-Schaltfläche'},
'Blok tekstowy': {'en': 'Text block', 'de': 'Textblock'},
'Katalog PDF': {'en': 'PDF Catalog', 'de': 'PDF-Katalog'},
'Tytuł / Tekst przycisku:': {'en': 'Title / Button text:', 'de': 'Titel / Schaltflächentext:'},
'Ikona:': {'en': 'Icon:', 'de': 'Symbol:'},
'Bez ikony': {'en': 'No icon', 'de': 'Kein Symbol'},
'URL / Numer / Email...': {'en': 'URL / Number / Email...', 'de': 'URL / Nummer / E-Mail...'},
'Prześlij PDF:': {'en': 'Upload PDF:', 'de': 'PDF hochladen:'},
'Zmień PDF (zastąpi obecny):': {'en': 'Upload PDF (replaces current):', 'de': 'PDF hochladen (ersetzt aktuelles):'},
'Tekst bloku:': {'en': 'Block text:', 'de': 'Blocktext:'},
'Dodaj': {'en': 'Add', 'de': 'Hinzufügen'},
'Czy na pewno usunąć?': {'en': 'Are you sure to delete?', 'de': 'Wirklich löschen?'},
'Edytuj': {'en': 'Edit', 'de': 'Bearbeiten'},
'Kategorie produktów/usług': {'en': 'Product Categories', 'de': 'Produktkategorien'},
'Dodaj nową kategorię': {'en': 'Add new category', 'de': 'Neue Kategorie hinzufügen'},
'Nazwa nowej kategorii:': {'en': 'New category name:', 'de': 'Name der neuen Kategorie:'},
'Produkty i Usługi': {'en': 'Products & Services', 'de': 'Produkte & Dienstleistungen'},
'Dodaj produkt/usługę': {'en': 'Add product/service', 'de': 'Produkt/Dienstleistung hinzufügen'},
'Nazwa *:': {'en': 'Name *:', 'de': 'Name *:'},
'Cena (0 aby ukryć):': {'en': 'Price (0 to hide):', 'de': 'Preis (0 zum Ausblenden):'},
'Zdjęcia (max 10):': {'en': 'Photos (max 10):', 'de': 'Fotos (max. 10):'},
'Opis:': {'en': 'Description:', 'de': 'Beschreibung:'},
'Kategoria:': {'en': 'Category:', 'de': 'Kategorie:'},
'Bez kategorii': {'en': 'Uncategorized', 'de': 'Nicht kategorisiert'},
'Szukaj': {'en': 'Search', 'de': 'Suchen'},
'Resetuj': {'en': 'Reset', 'de': 'Zurücksetzen'},
'Usuń': {'en': 'Delete', 'de': 'Löschen'},
'Brak rekordów.': {'en': 'No records found.', 'de': 'Keine Einträge gefunden.'},
'Zamówienie #': {'en': 'Order #', 'de': 'Bestellung #'},
'Faktura zamówienia': {'en': 'Order Invoice', 'de': 'Bestellrechnung'},
'Kupujący:': {'en': 'Buyer:', 'de': 'Käufer:'},
'Telefon:': {'en': 'Phone:', 'de': 'Telefon:'},
'Sprzedawca:': {'en': 'Seller:', 'de': 'Verkäufer:'},
'szt.': {'en': 'pcs', 'de': 'Stk.'},
'Wyślij do sprzedawcy': {'en': 'Send to seller', 'de': 'An Verkäufer senden'},
'Wróć do katalogu': {'en': 'Back to catalog', 'de': 'Zurück zum Katalog'},
'Wróć do panelu': {'en': 'Back to panel', 'de': 'Zurück zum Panel'},
'Generuj opis AI z pierwszego zdjęcia': {'en': 'Generate AI description from 1st photo', 'de': 'KI-Beschreibung aus 1. Foto generieren'},
'Zapisywanie...': {'en': 'Saving...', 'de': 'Speichern...'},
'Faktura': {'en': 'Invoice', 'de': 'Rechnung'},
'Cena:': {'en': 'Price:', 'de': 'Preis:'},
'Obecny awatar:': {'en': 'Current avatar:', 'de': 'Aktueller Avatar:'},
'Obecny PDF:': {'en': 'Current PDF:', 'de': 'Aktuelles PDF:'},
'Brak': {'en': 'None', 'de': 'Keine'},
'Ustawienia wizytówki': {'en': 'vCard Settings', 'de': 'Visitenkarten-Einstellungen'},
'Link': {'en': 'Link', 'de': 'Link'},
'Tekst': {'en': 'Text', 'de': 'Text'},
'Zmień zdjęcia (stare zostaną usunięte):': {'en': 'Replace photos (old will be removed):', 'de': 'Fotos ersetzen (alte werden entfernt):'},
'Utworzono wizytówkę ID:': {'en': 'Created vCard ID:', 'de': 'Visitenkarte erstellt ID:'},
'Zaktualizowano hasło dla:': {'en': 'Password updated for:', 'de': 'Passwort aktualisiert für:'},
'Nie znaleziono wizytówki:': {'en': 'vCard not found:', 'de': 'Visitenkarte nicht gefunden:'},
'Usunięto wizytówkę:': {'en': 'Deleted vCard:', 'de': 'Visitenkarte gelöscht:'},
'Nieprawidłowe hasło': {'en': 'Invalid password', 'de': 'Ungültiges Passwort'},
'Dodano blok.': {'en': 'Block added.', 'de': 'Block hinzugefügt.'},
'Zaktualizowano blok.': {'en': 'Block updated.', 'de': 'Block aktualisiert.'},
'Usunięto blok.': {'en': 'Block deleted.', 'de': 'Block gelöscht.'},
'Przeniesiono blok wyżej.': {'en': 'Block moved up.', 'de': 'Block nach oben verschoben.'},
'Przeniesiono blok niżej.': {'en': 'Block moved down.', 'de': 'Block nach unten verschoben.'},
'Dodano kategorię:': {'en': 'Category added:', 'de': 'Kategorie hinzugefügt:'},
'Nazwa kategorii nie może być pusta.': {'en': 'Category name cannot be empty.', 'de': 'Kategoriename darf nicht leer sein.'},
'Kategoria już istnieje:': {'en': 'Category already exists:', 'de': 'Kategorie existiert bereits:'},
'Usunięto kategorię:': {'en': 'Category deleted:', 'de': 'Kategorie gelöscht:'},
'Zaktualizowano pomyślnie.': {'en': 'Updated successfully.', 'de': 'Erfolgreich aktualisiert.'},
'Błąd podczas aktualizacji:': {'en': 'Error during update:', 'de': 'Fehler bei der Aktualisierung:'},
'Nazwa jest wymagana.': {'en': 'Name is required.', 'de': 'Name ist erforderlich.'},
'Zapisano:': {'en': 'Saved:', 'de': 'Gespeichert:'},
'Usunięto rekord:': {'en': 'Record deleted:', 'de': 'Eintrag gelöscht:'},
'Usunięto zamówienie z historii.': {'en': 'Order deleted from history.', 'de': 'Bestellung aus der Historie gelöscht.'},
'Błąd:': {'en': 'Error:', 'de': 'Fehler:'},
'Błąd ładowania PDF:': {'en': 'Error uploading PDF:', 'de': 'Fehler beim Hochladen von PDF:'},
'Awatar pomyślnie zaktualizowany.': {'en': 'Avatar successfully updated.', 'de': 'Avatar erfolgreich aktualisiert.'},
'Błąd podczas ładowania awatara:': {'en': 'Error uploading avatar:', 'de': 'Fehler beim Hochladen des Avatars:'},
'Zapisano ustawienia.': {'en': 'Settings saved.', 'de': 'Einstellungen gespeichert.'},
'Błąd usuwania:': {'en': 'Delete error:', 'de': 'Löschfehler:'},
'Nieznana akcja:': {'en': 'Unknown action:', 'de': 'Unbekannte Aktion:'},
'Błąd wykonania akcji:': {'en': 'Action execution error:', 'de': 'Fehler bei der Aktionsausführung:'},
'Czy na pewno usunąć tę wizytówkę? To działanie jest nieodwracalne.': {'en': 'Are you sure you want to delete this vCard? This action is irreversible.', 'de': 'Möchten Sie diese Visitenkarte wirklich löschen? Diese Aktion ist unumkehrbar.'},
'Czy na pewno? Produkty w tej kategorii zostaną oznaczone jako "Bez kategorii".': {'en': 'Are you sure? Products in this category will be marked as "Uncategorized".', 'de': 'Sind Sie sicher? Produkte in dieser Kategorie werden als "Nicht kategorisiert" markiert.'}
}
CURRENCIES = {
'PLN': 'Polski Złoty',
'EUR': 'Euro',
'USD': 'Dolar Amerykański',
'GBP': 'Funt Szterling',
'UAH': 'Hrywna ukraińska',
'CHF': 'Frank Szwajcarski'
}
COLOR_SCHEMES = {
'default': 'Turkusowy (domyślny)',
'forest': 'Leśna zieleń',
'ocean': 'Głęboki błękit',
'sunset': 'Zachód słońca',
'lavender': 'Lawenda',
'vintage': 'Vintage',
'dark': 'Ciemny (Północ)',
'cosmic': 'Kosmiczna noc',
'minty': 'Świeża mięta',
'mocha': 'Mokka',
'crimson': 'Karmazynowy świt',
'solar': 'Słoneczny blask',
'cyberpunk': 'Cyberpunk neon',
'neon': 'Neonowy błysk',
'pastel': 'Pastelowy (jasny)',
'emerald': 'Szmaragdowe miasto',
'gold': 'Luksusowe złoto',
'sakura': 'Kwiat wiśni',
'arctic': 'Arktyczny lód',
'volcano': 'Magma',
'monochrome_light': 'Klasyczny (Jasny)',
'monochrome_dark': 'Klasyczny (Ciemny)',
'nord': 'Skandynawski Nord',
'dracula': 'Dracula (Ciemny)',
'ruby': 'Głęboki Rubin',
'sapphire': 'Królewski Szafir',
'amethyst': 'Ametystowy blask'
}
ICONS = {
'fa-link': 'Link (strona www)',
'fa-phone': 'Telefon',
'fa-whatsapp': 'WhatsApp',
'fa-telegram': 'Telegram',
'fa-instagram': 'Instagram',
'fa-envelope': 'Email',
'fa-map-marker-alt': 'Lokalizacja/Adres',
'fa-youtube': 'YouTube',
'fa-tiktok': 'TikTok'
}
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def get_lang():
try:
from flask import session, request
return session.get('lang') or request.cookies.get('lang') or 'pl'
except:
return 'pl'
def t_text(pl_string):
lang = get_lang()
if lang == 'pl':
return pl_string
return TRANSLATIONS.get(pl_string, {}).get(lang, pl_string)
@app.context_processor
def inject_t():
return dict(t=t_text, current_lang=get_lang())
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:
local_path = 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": "Imię",
"vcard_lastname": "Nazwisko",
"vcard_job": "Specjalista",
"organization_name": "Moja Firma",
"currency_code": "PLN",
"chat_avatar": None,
"color_scheme": "default",
"admin_password_enabled": False,
"admin_password": "",
"categories_as_lines": False,
"about_text": "Cześć! To moja wirtualna wizytówka.",
"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 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)
def configure_gemini():
if not GOOGLE_API_KEY:
return False
try:
genai.configure(api_key=GOOGLE_API_KEY)
return True
except Exception:
return False
def generate_ai_description_from_image(image_data, language):
if not configure_gemini():
raise ValueError(t_text("Błąd: Google AI API nie jest skonfigurowane."))
try:
if not image_data:
raise ValueError(t_text("Błąd: Nie znaleziono pliku obrazu."))
image_stream = io.BytesIO(image_data)
image = Image.open(image_stream).convert('RGB')
except Exception:
raise ValueError(t_text("Błąd: Nie udało się przetworzyć obrazu."))
base_prompt = "Napisz atrakcyjny opis dla tego produktu/usługi. Tekst powinien być zwięzły, sprzedażowy, z użyciem emoji. Nie pisz cen, adresów ani telefonów."
if language == "English":
base_prompt = "Write an attractive description for this product/service. The text should be concise, sales-oriented, using emojis. Do not write prices, addresses, or phone numbers."
elif language == "Deutsch":
base_prompt = "Schreiben Sie eine attraktive Beschreibung für dieses Produkt/Dienstleistung. Der Text sollte prägnant, verkaufsorientiert und mit Emojis versehen sein. Schreiben Sie keine Preise, Adressen oder Telefonnummern."
try:
model = genai.GenerativeModel('gemma-4-31b-it')
response = model.generate_content([base_prompt, image])
if hasattr(response, 'text'):
return response.text
else:
if response.parts:
return "".join(part.text for part in response.parts if hasattr(part, 'text'))
else:
response.resolve()
return response.text
except Exception as e:
raise ValueError(f"{t_text('Błąd wykonania akcji:')} {e}")
LANDING_PAGE_TEMPLATE = '''
Platforma Wizytówek Online
'''
LOGIN_TEMPLATE = '''
{{ t('Zaloguj się do Panelu') }}
{{ t('Zaloguj') }}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
'''
ADMHOSTO_TEMPLATE = '''
{{ t('Główny Panel Administracyjny') }}
{{ t('Zarządzanie Wizytówkami') }}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
{{ t('Istniejące wizytówki') }}
{% if environments %}
{% else %}
{{ t('Nie utworzono jeszcze żadnych wizytówek.') }}
{% endif %}
'''
CATALOG_TEMPLATE = '''
{{ settings.vcard_firstname }} {{ settings.vcard_lastname }} - {{ t('Wizytówka') }}
{{ t('Zapisz w kontaktach') }}
{% if blocks %}
{% endif %}
{% if products_json != '[]' %}
{% endif %}
0
{{ t('Mój kod QR') }}
{{ t('Zeskanuj, aby otworzyć wizytówkę') }}
'''
ADMIN_TEMPLATE = '''
{{ t('Ustawienia wizytówki') }} - {{ settings.organization_name }}
{{ t('Zapisywanie...') }}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
{{ t('Profil i wygląd') }}
{{ t('Przyciski i Linki') }}
{{ t('Dodaj przycisk') }}
{% 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' %}{{ t('Link') }}{% elif block.type == 'pdf' %}PDF{% else %}{{ t('Tekst') }}{% endif %})
{% if block.type == 'link' %}
{{ block.url }}{% endif %}
{% endfor %}
{% else %}
{{ t('Brak rekordów.') }}
{% endif %}
{{ t('Kategorie produktów/usług') }}
{{ t('Dodaj nową kategorię') }}
{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}
{{ t('Brak rekordów.') }}
{% endif %}
{{ t('Produkty i Usługi') }}
{{ t('Dodaj produkt/usługę') }}
{{ t('Produkty i Usługi') }}:
{% if paginated_products %}
{% for product in paginated_products %}
{% if product.get('photos') %}

{% else %}
 }})
{% endif %}
{{ product['name'] }}
{{ t('Kategoria:') }} {{ product.get('category', 'Bez kategorii') }}
{{ t('Cena:') }} {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}{{ t('Brak') }}{% endif %}
{% endfor %}
{% if total_pages > 1 %}
{% endif %}
{% else %}
{{ t('Brak rekordów.') }}
{% endif %}
'''
ORDER_TEMPLATE = '''
{{ t('Zamówienie #') }}{{ order['id'] }}
{{ t('Kupujący:') }} {{ order['customer_name'] }}
{{ t('Telefon:') }} {{ order['customer_phone'] }}
{{ t('Sprzedawca:') }} {{ settings['organization_name'] }}
{% for pid, item in order['items'].items() %}
{% if item['photo'] %}
![{{ item['name'] }}](https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ item['photo'] }})
{% else %}
 }})
{% endif %}
{{ item['name'] }}
{{ item['qty'] }} {{ t('szt.') }} × {{ "%.2f"|format(item['price']|float) }} {{ currency }}
{{ "%.2f"|format(item['qty']|int * item['price']|float) }} {{ currency }}
{% endfor %}
{{ t('Razem:') }} {{ "%.2f"|format(order['total']|float) }} {{ currency }}
{% if send_link != "#" %}
{{ t('Wyślij do sprzedawcy') }} ({{ messenger_name }})
{% endif %}
{{ t('Wróć do katalogu') }}
'''
ADMIN_ORDERS_TEMPLATE = '''
{{ t('Historia zamówień') }} - {{ settings.organization_name }}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
{% if orders %}
{% for order in orders %}
{% for pid, item in order['items'].items() %}
{{ item['name'] }} ({{ item['qty'] }} {{ t('szt.') }})
{{ "%.2f"|format(item['price']|float * item['qty']|int) }} {{ currency }}
{% endfor %}
{{ t('Razem:') }} {{ "%.2f"|format(order['total']|float) }} {{ currency }}
{% endfor %}
{% else %}
{{ t('Brak rekordów.') }}
{% endif %}
'''
@app.route('/')
def index():
return render_template_string(LANDING_PAGE_TEMPLATE)
@app.route('/set_lang', methods=['POST'])
def set_lang():
lang = request.json.get('lang', 'pl')
session['lang'] = lang
resp = jsonify(success=True)
resp.set_cookie('lang', lang, max_age=60*60*24*365)
return resp
@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"Wizytówka {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": "Imię",
"vcard_lastname": "Nazwisko",
"vcard_job": "Specjalista",
"organization_name": "Moja Firma",
"currency_code": "PLN",
"chat_avatar": None,
"color_scheme": "default",
"admin_password_enabled": False,
"admin_password": "",
"categories_as_lines": False,
"about_text": "Cześć! To moja wirtualna wizytówka.",
"enable_cart": False,
"order_messenger": "whatsapp",
"order_contact": ""
}
}
save_data(all_data)
flash(f"{t_text('Utworzono wizytówkę 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"{t_text('Zaktualizowano hasło dla:')} {env_id}", 'success')
else:
flash(f"{t_text('Nie znaleziono wizytówki:')} {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"{t_text('Usunięto wizytówkę:')} {env_id}", 'success')
else:
flash(f"{t_text('Nie znaleziono wizytówki:')} {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(t_text('Nieprawidłowe hasło'), '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',[])
product_categories = set(p.get('category', t_text('Bez kategorii')) for p in all_products)
admin_categories = set(data.get('categories',[]))
all_cat_names = sorted(list(product_categories.union(admin_categories)))
products_sorted_for_js = sorted(all_products, key=lambda p: p.get('name', '').lower())
products_by_category = {cat:[] for cat in all_cat_names}
for product in all_products:
products_by_category[product.get('category', t_text('Bez kategorii'))].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
)
@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": t_text('Koszyk jest pusty')})
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 t_text("Brak wyników."), 404
currency = settings.get('currency_code', 'PLN')
lang = get_lang()
if lang == 'en':
text = f"Hello! I placed an order #{order['id']} for {order['total']} {currency}.\nInvoice link: {request.url}"
elif lang == 'de':
text = f"Hallo! Ich habe eine Bestellung #{order['id']} über {order['total']} {currency} aufgegeben.\nRechnungslink: {request.url}"
else:
text = f"Dzień dobry! Złożyłem zamówienie #{order['id']} na kwotę {order['total']} {currency}.\nLink do faktury: {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 = t_text("Brak")
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)
search_q = request.args.get('q', '').strip()
if request.method == 'POST':
action = request.form.get('action')
try:
if 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"{t_text('Błąd ładowania 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(t_text('Dodano blok.'), "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"{t_text('Błąd ładowania 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(t_text('Zaktualizowano blok.'), "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(t_text('Usunięto blok.'), "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(t_text('Przeniesiono blok wyżej.'), "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(t_text('Przeniesiono blok niżej.'), "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"{t_text('Dodano kategorię:')} {category_name}", 'success')
elif not category_name:
flash(t_text('Nazwa kategorii nie może być pusta.'), 'error')
else:
flash(f"{t_text('Kategoria już istnieje:')} {category_name}", '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'] = t_text('Bez kategorii')
data['categories'] = categories
data['products'] = products
save_env_data(env_id, data)
flash(f"{t_text('Usunięto kategorię:')} {category_to_delete}", 'success')
else:
flash(f"{t_text('Błąd usuwania:')} {category_to_delete}", 'error')
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', '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:
if 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)
flash(t_text("Awatar pomyślnie zaktualizowany."), 'success')
except Exception as e:
flash(f"{t_text('Błąd podczas ładowania awatara:')} {e}", 'error')
else:
pass
data['settings'] = settings
save_env_data(env_id, data)
flash(t_text("Zapisano ustawienia."), 'success')
elif action == 'add_product' or action == 'edit_product':
product_id = request.form.get('product_id')
product_data = {}
is_edit = action == 'edit_product'
if is_edit:
product_data = next((p for p in products if p.get('product_id') == product_id), None)
if not product_data:
flash(t_text("Błąd: nie znaleziono rekordu."), 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
else:
product_data['views'] = 0
product_data['name'] = request.form.get('name', '').strip()
product_data['description'] = request.form.get('description', '').strip()
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 t_text('Bez kategorii')
if not product_data['name']:
flash(t_text("Nazwa jest wymagana."), 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
photos_files = request.files.getlist('photos')
if photos_files and any(f.filename for f in photos_files):
if HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
new_photos_list = []
photo_limit = 10
uploaded_count = 0
for photo in photos_files:
if uploaded_count >= photo_limit: break
if photo and photo.filename:
try:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue
safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50]
photo_filename = f"{safe_name}_{uuid4().hex[:8]}{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)
new_photos_list.append(photo_filename)
os.remove(temp_path)
uploaded_count += 1
except Exception as e:
flash(f"{t_text('Błąd:')} {e}", 'error')
if new_photos_list and is_edit and 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
if new_photos_list:
product_data['photos'] = new_photos_list
else:
pass
if is_edit:
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
if product_index != -1:
products[product_index] = product_data
flash(f"{t_text('Zaktualizowano pomyślnie.')} '{product_data['name']}'", 'success')
else:
product_data['product_id'] = uuid4().hex
products.append(product_data)
flash(f"{t_text('Zapisano:')} '{product_data['name']}'", 'success')
data['products'] = products
save_env_data(env_id, data)
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:
flash(t_text("Błąd usuwania: nie znaleziono rekordu."), 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
deleted_product = products.pop(product_index)
product_name = deleted_product.get('name', 'N/A')
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(f"{t_text('Usunięto rekord:')} '{product_name}'", 'success')
else:
flash(f"{t_text('Nieznana akcja:')} {action}", 'warning')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
except Exception as e:
flash(f"{t_text('Błąd wykonania akcji:')} {e}", 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
filtered_products = products
if search_q:
q_lower = search_q.lower()
filtered_products = [p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()]
filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower())
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, search_q=search_q, 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, current_lang=get_lang()
)
@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', 'PLN'), current_lang=get_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(t_text("Usunięto zamówienie z historii."), "success")
return redirect(url_for('admin_orders', env_id=env_id))
@app.route('/generate_description_ai', methods=['POST'])
def handle_generate_description_ai():
request_data = request.get_json()
base64_image = request_data.get('image')
language = request_data.get('language', 'Polski')
if not base64_image: return jsonify({"error": t_text("Błąd: Nie znaleziono pliku obrazu.")}), 400
try:
image_data = base64.b64decode(base64_image)
result_text = generate_ai_description_from_image(image_data, language)
return jsonify({"text": result_text})
except ValueError as ve: return jsonify({"error": str(ve)}), 400
except Exception as e: return jsonify({"error": f"{t_text('Błąd:')} {e}"}), 500
if __name__ == '__main__':
configure_gemini()
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)