Eluza133 commited on
Commit
c9ba6aa
·
verified ·
1 Parent(s): 1f0e1b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +280 -5
app.py CHANGED
@@ -19,6 +19,7 @@ import zipfile
19
  import tempfile
20
  import pytz
21
  from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
 
22
 
23
  app = Flask(__name__)
24
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma")
@@ -391,6 +392,8 @@ def initialize_user_filesystem_tma(user_data, tma_user_id_str):
391
  }
392
  add_node(user_data['filesystem'], 'root', file_node)
393
  del user_data['files']
 
 
394
 
395
  @cache.memoize(timeout=300)
396
  def load_data():
@@ -561,10 +564,13 @@ def auth_via_telegram():
561
  user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
562
  user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
563
  user_info['reminders'] = []
 
564
  data['users'][tma_user_id_str] = user_info
565
  initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
566
  else:
567
  data['users'][tma_user_id_str].update(user_info)
 
 
568
 
569
  try: save_data(data)
570
  except Exception as e:
@@ -586,7 +592,9 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
586
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
587
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
588
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
589
- <style>''' + BASE_STYLE + '''</style></head><body>
 
 
590
  <div class="app-header">
591
  <div class="user-info">{{ display_name }}</div>
592
  <div class="view-toggle">
@@ -684,6 +692,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
684
  <div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
685
  <div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
686
  <div class="fab-option" id="fab-option-shoppinglist" onclick="openListEditorModal(null, 'shoppinglist')"><i class="fa-solid fa-cart-shopping"></i><span>Покупки</span></div>
 
687
  </div>
688
  <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
689
  <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
@@ -1536,7 +1545,6 @@ def batch_delete_tma():
1536
  if node_type == 'folder':
1537
  if node.get('children'): errors.append(f'Папка "{node_name}" не пуста.'); continue
1538
 
1539
- # For all types that can be deleted
1540
  if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file']:
1541
  if node_type == 'file':
1542
  try:
@@ -2506,7 +2514,7 @@ def admin_delete_item(tma_user_id_str, item_id):
2506
  api = HfApi()
2507
  if hf_path: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
2508
  except hf_utils.EntryNotFoundError:
2509
- pass # Already deleted from remote, just remove from DB
2510
  except Exception as e:
2511
  flash(f'Deletion error from remote storage: {e}', 'error')
2512
  return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
@@ -2514,7 +2522,6 @@ def admin_delete_item(tma_user_id_str, item_id):
2514
  flash('Folder is not empty.', 'error')
2515
  return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
2516
 
2517
- # If we are here, we can delete the node from filesystem
2518
  if remove_node(user_data['filesystem'], item_id)[0]:
2519
  try:
2520
  save_data(data)
@@ -2547,6 +2554,275 @@ def admin_delete_reminder(tma_user_id_str, reminder_id):
2547
  flash('Reminder not found.', 'error')
2548
  return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
2549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2550
  if __name__ == '__main__':
2551
  if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
2552
  if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Downloads/previews might fail.")
@@ -2566,4 +2842,3 @@ if __name__ == '__main__':
2566
  threading.Thread(target=check_reminders, daemon=True).start()
2567
 
2568
  app.run(debug=False, host='0.0.0.0', port=7860)
2569
-
 
19
  import tempfile
20
  import pytz
21
  from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
22
+ import re
23
 
24
  app = Flask(__name__)
25
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma")
 
392
  }
393
  add_node(user_data['filesystem'], 'root', file_node)
394
  del user_data['files']
395
+ if 'business_profiles' not in user_data:
396
+ user_data['business_profiles'] = []
397
 
398
  @cache.memoize(timeout=300)
399
  def load_data():
 
564
  user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
565
  user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
566
  user_info['reminders'] = []
567
+ user_info['business_profiles'] = []
568
  data['users'][tma_user_id_str] = user_info
569
  initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
570
  else:
571
  data['users'][tma_user_id_str].update(user_info)
572
+ if 'business_profiles' not in data['users'][tma_user_id_str]:
573
+ data['users'][tma_user_id_str]['business_profiles'] = []
574
 
575
  try: save_data(data)
576
  except Exception as e:
 
592
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
593
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
594
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
595
+ <style>''' + BASE_STYLE + '''
596
+ #fab-option-business i { color: #4caf50; }
597
+ </style></head><body>
598
  <div class="app-header">
599
  <div class="user-info">{{ display_name }}</div>
600
  <div class="view-toggle">
 
692
  <div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
693
  <div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
694
  <div class="fab-option" id="fab-option-shoppinglist" onclick="openListEditorModal(null, 'shoppinglist')"><i class="fa-solid fa-cart-shopping"></i><span>Покупки</span></div>
695
+ <a href="{{ url_for('tma_business_profiles') }}" class="fab-option" id="fab-option-business"><i class="fa-solid fa-briefcase"></i><span>Бизнес</span></a>
696
  </div>
697
  <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
698
  <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
 
1545
  if node_type == 'folder':
1546
  if node.get('children'): errors.append(f'Папка "{node_name}" не пуста.'); continue
1547
 
 
1548
  if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file']:
1549
  if node_type == 'file':
1550
  try:
 
2514
  api = HfApi()
2515
  if hf_path: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
2516
  except hf_utils.EntryNotFoundError:
2517
+ pass
2518
  except Exception as e:
2519
  flash(f'Deletion error from remote storage: {e}', 'error')
2520
  return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
 
2522
  flash('Folder is not empty.', 'error')
2523
  return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
2524
 
 
2525
  if remove_node(user_data['filesystem'], item_id)[0]:
2526
  try:
2527
  save_data(data)
 
2554
  flash('Reminder not found.', 'error')
2555
  return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
2556
 
2557
+ def find_business_profile_by_login(login):
2558
+ data = load_data()
2559
+ for user_id, user_data in data.get('users', {}).items():
2560
+ for profile in user_data.get('business_profiles', []):
2561
+ if profile.get('login') == login:
2562
+ return profile, user_data
2563
+ return None, None
2564
+
2565
+ def is_business_login_unique(login, user_id, profile_id=None):
2566
+ data = load_data()
2567
+ for uid, udata in data.get('users', {}).items():
2568
+ for profile in udata.get('business_profiles', []):
2569
+ if profile.get('login') == login:
2570
+ if uid == user_id and profile.get('id') == profile_id:
2571
+ continue
2572
+ return False
2573
+ return True
2574
+
2575
+ BUSINESS_PROFILES_LIST_HTML = '''
2576
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2577
+ <title>Бизнес-страницы</title>
2578
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2579
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
2580
+ <style>''' + BASE_STYLE + '''
2581
+ .profile-list-item { background: var(--card-bg-dark); border-radius: 12px; margin-bottom: 10px; padding: 15px; display: flex; justify-content: space-between; align-items: center; }
2582
+ .profile-details { text-align: left; }
2583
+ .profile-details a { color: var(--text-dark); text-decoration: none; font-weight: 600; }
2584
+ .profile-details small { color: var(--text-muted); display: block; }
2585
+ .profile-actions { display: flex; gap: 10px; }
2586
+ </style></head><body>
2587
+ <div class="app-header">
2588
+ <div class="user-info">{{ display_name }}</div>
2589
+ <a href="{{ url_for('tma_dashboard') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
2590
+ </div>
2591
+ <div class="container">
2592
+ <h2>Мои бизнес-страницы</h2>
2593
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2594
+ {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2595
+ {% endif %}{% endwith %}
2596
+ <a href="{{ url_for('tma_create_business_profile') }}" class="btn" style="width: 100%; margin-bottom: 20px;">Создать новую страницу</a>
2597
+ <div class="profile-list">
2598
+ {% for profile in profiles %}
2599
+ <div class="profile-list-item">
2600
+ <div class="profile-details">
2601
+ <a href="{{ url_for('tma_manage_business_profile', profile_id=profile.id) }}"><strong>{{ profile.org_name }}</strong></a>
2602
+ <small>/biz/{{ profile.login }}</small>
2603
+ </div>
2604
+ <div class="profile-actions">
2605
+ <a href="{{ url_for('tma_manage_business_profile', profile_id=profile.id) }}" class="btn" style="padding: 8px 12px;"><i class="fa-solid fa-store"></i></a>
2606
+ </div>
2607
+ </div>
2608
+ {% else %}
2609
+ <p>У вас еще нет бизнес-страниц.</p>
2610
+ {% endfor %}
2611
+ </div>
2612
+ </div>
2613
+ <script>
2614
+ window.Telegram.WebApp.ready();
2615
+ window.Telegram.WebApp.BackButton.show();
2616
+ window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_dashboard') }}"; });
2617
+ </script>
2618
+ </body></html>
2619
+ '''
2620
+
2621
+ CREATE_EDIT_BUSINESS_PROFILE_HTML = '''
2622
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2623
+ <title>{{ 'Редактировать' if profile else 'Создать' }} бизнес-страницу</title>
2624
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2625
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
2626
+ <style>''' + BASE_STYLE + '''</style></head><body>
2627
+ <div class="app-header">
2628
+ <div class="user-info">{{ display_name }}</div>
2629
+ <a href="{{ url_for('tma_business_profiles') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
2630
+ </div>
2631
+ <div class="container">
2632
+ <h2>{{ 'Редактировать' if profile else 'Создать' }} бизнес-страницу</h2>
2633
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2634
+ {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2635
+ {% endif %}{% endwith %}
2636
+ <form method="post" enctype="multipart/form-data">
2637
+ <label for="org_name">Название организации</label>
2638
+ <input type="text" name="org_name" value="{{ profile.org_name or '' }}" required>
2639
+
2640
+ <label for="login">Логин (URL)</label>
2641
+ <input type="text" name="login" value="{{ profile.login or '' }}" required pattern="[a-zA-Z0-9_.-]+" title="Только латинские буквы, цифры и символы _, -, .">
2642
+
2643
+ <label for="avatar">Аватар (необязательно)</label>
2644
+ <input type="file" name="avatar" accept="image/*">
2645
+
2646
+ <label for="currency">Валюта</label>
2647
+ <select name="currency" required>
2648
+ {% set currencies = {'тенге': 'KZT', 'рубль': 'RUB', 'кыргызский сом': 'KGS', 'узбекский сум': 'UZS', 'украинская гривна': 'UAH'} %}
2649
+ {% for name, code in currencies.items() %}
2650
+ <option value="{{ code }}" {% if profile and profile.currency == code %}selected{% endif %}>{{ name }}</option>
2651
+ {% endfor %}
2652
+ </select>
2653
+
2654
+ <label>
2655
+ <input type="checkbox" name="show_prices" value="true" {% if profile and profile.show_prices %}checked{% endif %} style="width: auto; margin-right: 10px;">
2656
+ Указывать цены
2657
+ </label>
2658
+
2659
+ <label for="order_destination">Куда будут приходить заказы?</label>
2660
+ <select name="order_destination" required>
2661
+ <option value="whatsapp" {% if profile and profile.order_destination == 'whatsapp' %}selected{% endif %}>WhatsApp</option>
2662
+ <option value="telegram" {% if profile and profile.order_destination == 'telegram' %}selected{% endif %}>Telegram</option>
2663
+ </select>
2664
+
2665
+ <label for="contact_info">Номер телефона или username</label>
2666
+ <input type="text" name="contact_info" value="{{ profile.contact_info or '' }}" placeholder="+7 (XXX) XXX-XX-XX или @username" required>
2667
+
2668
+ <button type="submit" class="btn" style="width: 100%; margin-top: 20px;">Сохранить</button>
2669
+ </form>
2670
+ </div>
2671
+ <script>
2672
+ window.Telegram.WebApp.ready();
2673
+ window.Telegram.WebApp.BackButton.show();
2674
+ window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_business_profiles') }}"; });
2675
+ </script>
2676
+ </body></html>
2677
+ '''
2678
+
2679
+ MANAGE_BUSINESS_PROFILE_HTML = '''
2680
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2681
+ <title>Управление: {{ profile.org_name }}</title>
2682
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2683
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
2684
+ <style>''' + BASE_STYLE + '''
2685
+ .profile-header { text-align: center; margin-bottom: 20px; }
2686
+ .profile-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; background: #333; }
2687
+ .public-link-bar { background: var(--card-bg-dark); padding: 10px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
2688
+ .public-link-bar input { background: transparent; border: none; color: var(--text-muted); margin: 0; padding: 0; }
2689
+ .product-card { background: var(--card-bg-dark); border-radius: 12px; margin-bottom: 15px; padding: 15px; text-align: left; }
2690
+ .product-card img { max-width: 100%; border-radius: 8px; margin-bottom: 10px; }
2691
+ .product-actions { display: flex; gap: 10px; margin-top: 15px; }
2692
+ .add-product-form { background: var(--card-bg-dark); padding: 20px; border-radius: 12px; margin-top: 25px; }
2693
+ </style></head><body>
2694
+ <div class="app-header">
2695
+ <div class="user-info">{{ display_name }}</div>
2696
+ <a href="{{ url_for('tma_business_profiles') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
2697
+ </div>
2698
+ <div class="container">
2699
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2700
+ {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2701
+ {% endif %}{% endwith %}
2702
+ <div class="profile-header">
2703
+ {% if profile.avatar_path %}
2704
+ <img src="{{ hf_file_url_jinja(profile.avatar_path) }}" class="profile-avatar">
2705
+ {% else %}
2706
+ <div class="profile-avatar" style="display: inline-flex; align-items: center; justify-content: center; font-size: 3em; color: var(--primary);">{{ profile.org_name[0] }}</div>
2707
+ {% endif %}
2708
+ <h2>{{ profile.org_name }}</h2>
2709
+ <a href="{{ url_for('tma_edit_business_profile', profile_id=profile.id) }}" class="btn" style="padding: 8px 15px; font-size: 0.9em;">Редактировать</a>
2710
+ </div>
2711
+
2712
+ <div class="public-link-bar">
2713
+ <input type="text" value="{{ url_for('public_business_page', login=profile.login, _external=True) }}" readonly>
2714
+ <button class="btn" onclick="copyToClipboard('{{ url_for('public_business_page', login=profile.login, _external=True) }}')" style="padding: 8px 12px;"><i class="fa-solid fa-copy"></i></button>
2715
+ </div>
2716
+
2717
+ <h3>Товары</h3>
2718
+ <div class="products-list">
2719
+ {% for product in profile.products %}
2720
+ <div class="product-card">
2721
+ {% if product.photo_path %}<img src="{{ hf_file_url_jinja(product.photo_path) }}">{% endif %}
2722
+ <h4>{{ product.name }}</h4>
2723
+ {% if profile.show_prices %}<p style="color: var(--secondary); font-weight: 600;">{{ "%.2f"|format(product.price|float) }} {{ profile.currency }}</p>{% endif %}
2724
+ <p style="white-space: pre-wrap; color: var(--text-muted);">{{ product.description }}</p>
2725
+ <div class="product-actions">
2726
+ <form method="post" action="{{ url_for('tma_delete_product', profile_id=profile.id, product_id=product.id) }}" onsubmit="return confirm('Удалить этот товар?');">
2727
+ <button type="submit" class="btn delete-btn" style="padding: 8px 15px;">Удалить</button>
2728
+ </form>
2729
+ </div>
2730
+ </div>
2731
+ {% else %}
2732
+ <p>Вы еще не добавили ни одного товара.</p>
2733
+ {% endfor %}
2734
+ </div>
2735
+
2736
+ <div class="add-product-form">
2737
+ <h4>Добавить новый товар</h4>
2738
+ <form method="post" action="{{ url_for('tma_add_product', profile_id=profile.id) }}" enctype="multipart/form-data">
2739
+ <label for="product_name">Название товара</label>
2740
+ <input type="text" name="product_name" required>
2741
+
2742
+ <label for="product_photo">Фото товара</label>
2743
+ <input type="file" name="product_photo" accept="image/*" required>
2744
+
2745
+ <label for="product_description">Описание</label>
2746
+ <textarea name="product_description" rows="4"></textarea>
2747
+
2748
+ {% if profile.show_prices %}
2749
+ <label for="product_price">Цена ({{ profile.currency }})</label>
2750
+ <input type="number" name="product_price" step="0.01" min="0">
2751
+ {% endif %}
2752
+
2753
+ <button type="submit" class="btn" style="width: 100%; margin-top: 15px;">Добавить товар</button>
2754
+ </form>
2755
+ </div>
2756
+ </div>
2757
+ <script>
2758
+ window.Telegram.WebApp.ready();
2759
+ window.Telegram.WebApp.BackButton.show();
2760
+ window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_business_profiles') }}"; });
2761
+
2762
+ function copyToClipboard(text) {
2763
+ navigator.clipboard.writeText(text).then(() => {
2764
+ window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
2765
+ window.Telegram.WebApp.showAlert('Ссылка скопирована!');
2766
+ }, () => {
2767
+ window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
2768
+ });
2769
+ }
2770
+ </script>
2771
+ </body></html>
2772
+ '''
2773
+
2774
+ PUBLIC_BUSINESS_PAGE_HTML = '''
2775
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
2776
+ <title>{{ profile.org_name }}</title>
2777
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2778
+ <style>''' + BASE_STYLE + '''
2779
+ body { padding-bottom: 80px; }
2780
+ .public-header { padding: 20px; text-align: center; }
2781
+ .public-avatar { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; margin-bottom: 15px; border: 3px solid var(--card-bg-dark); }
2782
+ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
2783
+ .product-public-card { background: var(--card-bg-dark); border-radius: 16px; overflow: hidden; text-align: left; }
2784
+ .product-public-card img { width: 100%; height: 200px; object-fit: cover; display: block; }
2785
+ .product-public-info { padding: 15px; }
2786
+ .product-public-info h4 { font-size: 1.2em; margin-bottom: 5px; }
2787
+ .product-public-info .price { font-size: 1.1em; font-weight: 600; color: var(--secondary); margin-bottom: 10px; }
2788
+ .product-public-info .desc { color: var(--text-muted); font-size: 0.9em; }
2789
+ .order-button-container { position: fixed; bottom: 0; left: 0; right: 0; padding: 15px; background: var(--glass-bg); backdrop-filter: blur(10px); text-align: center; }
2790
+ </style></head><body>
2791
+ <div class="public-header">
2792
+ {% if profile.avatar_path %}
2793
+ <img src="{{ hf_file_url_jinja(profile.avatar_path) }}" class="public-avatar">
2794
+ {% else %}
2795
+ <div class="public-avatar" style="display: inline-flex; align-items: center; justify-content: center; font-size: 4em; color: var(--primary);">{{ profile.org_name[0] }}</div>
2796
+ {% endif %}
2797
+ <h1>{{ profile.org_name }}</h1>
2798
+ </div>
2799
+ <div class="container" style="padding-top: 0;">
2800
+ <div class="product-grid">
2801
+ {% for product in profile.products %}
2802
+ <div class="product-public-card">
2803
+ {% if product.photo_path %}<img src="{{ hf_file_url_jinja(product.photo_path) }}">{% endif %}
2804
+ <div class="product-public-info">
2805
+ <h4>{{ product.name }}</h4>
2806
+ {% if profile.show_prices and product.price is not none %}
2807
+ <p class="price">{{ "%.2f"|format(product.price|float) }} {{ profile.currency }}</p>
2808
+ {% endif %}
2809
+ <p class="desc">{{ product.description }}</p>
2810
+ </div>
2811
+ </div>
2812
+ {% else %}
2813
+ <p style="grid-column: 1 / -1; text-align: center;">Товары скоро появятся.</p>
2814
+ {% endfor %}
2815
+ </div>
2816
+ </div>
2817
+ <div class="order-button-container">
2818
+ <a href="{{ order_link }}" target="_blank" class="btn" style="width: 100%; max-width: 400px; background: {{ '#25D366' if profile.order_destination == 'whatsapp' else '#0088cc' }};">
2819
+ <i class="fa-brands fa-{{ 'whatsapp' if profile.order_destination == 'whatsapp' else 'telegram' }}"></i>
2820
+ Заказать в {{ profile.order_destination.capitalize() }}
2821
+ </a>
2822
+ </div>
2823
+ </body></html>
2824
+ '''
2825
+
2826
  if __name__ == '__main__':
2827
  if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
2828
  if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Downloads/previews might fail.")
 
2842
  threading.Thread(target=check_reminders, daemon=True).start()
2843
 
2844
  app.run(debug=False, host='0.0.0.0', port=7860)