Optomshop / app.py
Kgshop's picture
Update app.py
3dde081 verified
from flask import Flask, render_template_string, request, redirect, url_for, send_file, jsonify
import json
import os
import logging
import threading
import time
from datetime import datetime, timezone, timedelta
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError
from werkzeug.utils import secure_filename
import pandas as pd
from io import BytesIO
import requests
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
DATA_FILE = 'data_optomshop.json'
ORDERS_FILE = 'orders_optomshop.json'
REPO_ID = "Kgshop/optomshop"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
WHATSAPP_NUMBER = "996703103679"
logging.basicConfig(level=logging.DEBUG)
BISHKEK_TIMEZONE = timezone(timedelta(hours=6))
def load_data():
try:
download_db_from_hf()
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
return {'products': [], 'categories': []}
if data['categories'] and isinstance(data['categories'][0], str):
data['categories'] = [{'name': cat, 'subcategories': []} for cat in data['categories']]
return data
except (FileNotFoundError, json.JSONDecodeError, RepositoryNotFoundError) as e:
logging.warning(f"Problem loading data (using empty values): {e}")
return {'products': [], 'categories': []}
except Exception as e:
logging.error(f"Unexpected error loading data: {e}")
return {'products': [], 'categories': []}
def save_data(data):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
upload_db_to_hf()
except Exception as e:
logging.error(f"Error saving data: {e}")
def load_orders():
try:
if not os.path.exists(ORDERS_FILE):
return []
with open(ORDERS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
logging.warning(f"Problem loading orders: {e}")
return []
except Exception as e:
logging.error(f"Error loading orders: {e}")
return []
def save_orders(orders):
try:
with open(ORDERS_FILE, 'w', encoding='utf-8') as f:
json.dump(orders, f, ensure_ascii=False, indent=4)
except Exception as e:
logging.error(f"Error saving orders: {e}")
def upload_db_to_hf():
try:
if not HF_TOKEN_WRITE:
raise ValueError("HF_TOKEN_WRITE not set.")
api = HfApi()
api.upload_file(
path_or_fileobj=DATA_FILE,
path_in_repo=DATA_FILE,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Automatic backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info("Database uploaded to HF Hub.")
except Exception as e:
logging.error(f"Error uploading to HF Hub: {e}")
def download_db_from_hf():
try:
if not HF_TOKEN_READ:
raise ValueError("HF_TOKEN_READ not set")
hf_hub_download(
repo_id=REPO_ID,
filename=DATA_FILE,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
local_dir_use_symlinks=False
)
logging.info("Database downloaded from HF Hub.")
except RepositoryNotFoundError as e:
logging.error(f"Repository not found: {e}")
raise
except Exception as e:
logging.error(f"Error downloading from HF Hub: {e}")
raise
def periodic_backup():
while True:
upload_db_to_hf()
time.sleep(800)
@app.route('/')
def index():
return redirect(url_for('catalog'))
@app.route('/catalog')
def catalog():
data = load_data()
return render_template_string(get_catalog_template(), products=data['products'], categories=data['categories'], repo_id=REPO_ID, current_category="all")
@app.route('/catalog/<category>')
def category_catalog(category):
data = load_data()
filtered_products = []
if ':' in category:
main_category, subcategory = category.split(':', 1)
main_category = main_category.strip().lower()
subcategory = subcategory.strip().lower()
filtered_products = [
p for p in data['products']
if str(p.get('category', '')).strip().lower() == main_category and str(p.get('subcategory', '')).strip().lower() == subcategory
]
else:
category = category.strip().lower()
filtered_products = [
p for p in data['products']
if str(p.get('category', '')).strip().lower() == category
]
return render_template_string(get_catalog_template(), products=filtered_products, categories=data['categories'], repo_id=REPO_ID, current_category=category)
def get_catalog_template():
return '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Optomshop - мужская одежда, футболки оптом</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #f0f2f5, #e9ecef);
color: #2d3748;
line-height: 1.6;
margin: 0;
}
.container {
max-width: 1300px;
margin: 0 auto;
padding: 20px;
padding-top: 0;
}
.header {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 0;
background-image: url('https://huggingface.co/spaces/Kgshop/Optomshop/resolve/main/IMG-20250329-WA0068.jpg');
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
border-bottom: 2px solid #e2e8f0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
height: 200px;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(16, 185, 129, 0.3));
z-index: 1;
opacity: 0.5;
}
.section-title {
text-align: center;
font-size: 1.2rem;
font-weight: 500;
color: #2d3748;
margin: 15px 0;
}
.social-icons {
display: flex;
justify-content: center;
gap: 15px;
margin: 5px 0;
flex-wrap: wrap;
}
.social-icon {
font-size: 1.5rem;
color: #2d3748;
transition: color 0.3s ease;
}
.social-icon:hover {
color: #3b82f6;
}
hr {
border: 0;
height: 1px;
background: rgba(45, 55, 72, 0.3);
margin: 20px 0;
}
.category-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
padding: 10px;
}
.category-button {
padding: 15px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #fff;
text-align: center;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.category-button:hover {
background-color: #f0f2f5;
border-color: #3b82f6;
}
.subcategory-list {
display: none;
padding: 10px;
background-color: #f9fafb;
border-radius: 8px;
margin-top: 5px;
}
.category-button.active + .subcategory-list {
display: block;
}
.subcategory-item {
padding: 10px;
border-bottom: 1px solid #e2e8f0;
cursor: pointer;
transition: background-color 0.3s ease;
}
.subcategory-item:last-child {
border-bottom: none;
}
.subcategory-item:hover {
background-color: #e2e8f0;
}
.products-grid {
display: grid;
grid-template-columns: repeat(2, minmax(200px, 1fr));
gap: 15px;
padding: 10px;
}
.product {
background: #fff;
border-radius: 15px;
padding: 15px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
overflow: hidden;
cursor: pointer;
}
.product:hover {
transform: translateY(-5px) scale(1.02);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.product-image {
width: 100%;
aspect-ratio: 1;
background-color: #fff;
border-radius: 10px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.product-image img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
}
.product-image img:hover {
transform: scale(1.1);
}
.product h2 {
font-size: 1rem;
font-weight: 600;
margin: 10px 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-price {
font-size: 1.1rem;
color: #ef4444;
font-weight: 700;
text-align: center;
margin: 5px 0;
}
.product-description {
font-size: 0.8rem;
color: #68096;
text-align: center;
margin-bottom: 15px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-button {
display: block;
width: 100%;
padding: 8px;
border: none;
border-radius: 8px;
background-color: #3b82f6;
color: white;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin: 5px 0;
text-align: center;
text-decoration: none;
}
.product-button:hover {
background-color: #2563eb;
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
transform: translateY(-2px);
}
.add-to-cart {
background-color: #10b981;
}
.add-to-cart:hover {
background-color: #059669;
box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
}
#cart-button {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #ef4444;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 1.2rem;
cursor: pointer;
display: none;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
}
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #ffffff;
margin: 15% auto;
padding: 20px;
border-radius: 15px;
width: 90%;
max-width: 500px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
animation: slideIn 0.3s ease-out;
overflow-y: auto;
max-height: 70vh;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.close {
float: right;
font-size: 1.5rem;
color: #718096;
cursor: pointer;
transition: color 0.3s;
}
.close:hover {
color: #2d3748;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #e2e8f0;
}
.cart-item img {
width: 50px;
height: 50px;
object-fit: contain;
border-radius: 8px;
margin-right: 15px;
}
.quantity-input, .color-select {
width: 100%;
max-width: 150px;
padding: 8px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
margin: 5px 0;
}
.clear-cart {
background-color: #ef4444;
}
.clear-cart:hover {
background-color: #dc2626;
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
}
.order-button {
background-color: #10b981;
}
.order-button:hover {
background-color: #059669;
box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
}
.remove-item-btn {
background-color: #ef4444;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 0.8rem;
transition: background-color 0.3s;
}
.remove-item-btn:hover {
background-color: #dc2626;
}
.social-button {
padding: 10px 20px;
border: none;
border-radius: 8px;
color: white;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: flex;
align-items: center;
gap: 5px;
width: 100%;
justify-content: center;
margin: 5px 0;
}
.social-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.call-button { background-color: #4CAF50; }
.whatsapp-button { background-color: #25D366; }
.instagram-button { background-color: #E1306C; }
.telegram-button { background-color: #0088cc; }
.whatsapp-group-button { background-color: #25D366; }
.location-button { background-color: #FF9800; }
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<div class="section-title">Контакты</div>
<div class="social-icons">
<a href="tel:+996703103679" class="social-icon"><i class="fas fa-phone"></i></a>
<a href="https://instagram.com/optomshop.kg" target="_blank" class="social-icon"><i class="fab fa-instagram"></i></a>
<a href="https://t.me/+5b2MhBek3SU5YjA6" target="_blank" class="social-icon"><i class="fab fa-telegram"></i></a>
<a href="https://api.whatsapp.com/send?phone=996703103679" target="_blank" class="social-icon"><i class="fab fa-whatsapp"></i></a>
<a href="https://2gis.kg/bishkek/geo/70030076288068720/74.616388,42.940167" target="_blank" class="social-icon"><i class="fas fa-map-marker-alt"></i></a>
</div>
<hr>
<div class="section-title">Группы</div>
<div class="social-icons">
<a href="https://chat.whatsapp.com/LnY1q03GxTo43ccuNoy8MB" target="_blank" class="social-icon"><i class="fab fa-whatsapp"></i></a>
<a href="https://t.me/+5b2MhBek3SU5YjA6" target="_blank" class="social-icon"><i class="fab fa-telegram"></i></a>
</div>
<hr>
<div class="category-buttons">
<div class="category-button" onclick="window.location.href='/catalog'">Все категории</div>
{% for category in categories %}
<div class="category-button" onclick="toggleSubcategories(this)">
{{ category['name'] }}
</div>
{% if category['subcategories'] %}
<div class="subcategory-list">
{% for subcat in category['subcategories'] %}
<div class="subcategory-item" onclick="window.location.href='/catalog/{{ category['name'] }}:{{ subcat }}'">{{ subcat }}</div>
{% endfor %}
<div class="subcategory-item" onclick="window.location.href='/catalog/{{ category['name'] }}'">Все в {{ category['name'] }}</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="products-grid" id="products-grid">
{% for product in products %}
<div class="product"
data-name="{{ product['name']|lower }}"
data-description="{{ product['description']|lower }}"
data-category="{{ product.get('category', 'Без категории') }}"
onclick="window.location.href='/product/{{ loop.index0 }}'">
{% if product.get('photos') and product['photos']|length > 0 %}
<div class="product-image">
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
alt="{{ product['name'] }}"
loading="lazy">
</div>
{% endif %}
<h2>{{ product['name'] }}</h2>
<div class="product-price">{{ product['price']|int }} сом</div>
<p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
<button class="product-button add-to-cart" onclick="event.stopPropagation(); openQuantityModal({{ loop.index0 }})">В корзину</button>
</div>
{% endfor %}
</div>
</div>
<div id="quantityModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('quantityModal')">×</span>
<h2>Укажите количество и цвет</h2>
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
<select id="colorSelect" class="color-select"></select>
<button class="product-button" onclick="confirmAddToCart()">Добавить</button>
</div>
</div>
<div id="cartModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('cartModal')">×</span>
<h2>Корзина</h2>
<div id="cartContent"></div>
<div style="margin-top: 20px; text-align: right;">
<strong>Итого: <span id="cartTotal">0</span> сом</strong>
<button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
<button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
</div>
</div>
</div>
<button id="cart-button" onclick="openCartModal()">🛒</button>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
<script>
const products = {{ products|tojson }};
let selectedProductIndex = null;
function toggleSubcategories(element) {
element.classList.toggle('active');
}
function openModal(modalId) {
document.getElementById(modalId).style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function openQuantityModal(index) {
selectedProductIndex = index;
const product = products[index];
const colorSelect = document.getElementById('colorSelect');
colorSelect.innerHTML = '';
if (product.colors && product.colors.length > 0) {
product.colors.forEach(color => {
const option = document.createElement('option');
option.value = color;
option.text = color;
colorSelect.appendChild(option);
});
} else {
const option = document.createElement('option');
option.value = 'Нет цвета';
option.text = 'Нет цвета';
colorSelect.appendChild(option);
}
document.getElementById('quantityModal').style.display = 'block';
document.getElementById('quantityInput').value = 1;
}
function confirmAddToCart() {
if (selectedProductIndex === null) return;
const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
const color = document.getElementById('colorSelect').value;
if (quantity <= 0) {
alert("Укажите количество больше 0");
return;
}
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
const product = products[selectedProductIndex];
const cartItemId = `${product.name}-${color}`;
const existingItem = cart.find(item => item.id === cartItemId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({
id: cartItemId,
name: product.name,
price: product.price,
photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
quantity: quantity,
color: color
});
}
localStorage.setItem('cart', JSON.stringify(cart));
closeModal('quantityModal');
updateCartButton();
}
function updateCartButton() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
}
function openCartModal() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartContent = document.getElementById('cartContent');
let total = 0;
cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
return `
<div class="cart-item">
<div style="display: flex; align-items: center;">
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">
<div>
<strong>${item.name}</strong>
<p>${item.price} сом × ${item.quantity} (Цвет: ${item.color})</p>
</div>
</div>
<div>
<span>${itemTotal} сом</span>
<button class="remove-item-btn" onclick="removeItem('${item.id}')">Удалить</button>
</div>
</div>
`;
}).join('');
document.getElementById('cartTotal').textContent = total;
document.getElementById('cartModal').style.display = 'block';
}
function removeItem(itemId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.id !== itemId);
localStorage.setItem('cart', JSON.stringify(cart));
openCartModal();
updateCartButton();
}
function orderViaWhatsApp() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert("Корзина пуста!");
return;
}
let total = 0;
let orderText = "Заказ:%0A";
cart.forEach((item, index) => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
orderText += `${index + 1}. ${item.name} - ${item.price} сом × ${item.quantity} (Цвет: ${item.color})%0A`;
});
orderText += `Итого: ${total} сом`;
window.open(`https://api.whatsapp.com/send?phone=996703103679&text=${orderText}`, '_blank');
saveOrder(cart, total);
}
function saveOrder(cart, total) {
const order = {
timestamp: new Date().toISOString(),
items: cart,
total: total
};
fetch('/save_order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(order),
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Заказ сохранен:', data);
localStorage.removeItem('cart');
updateCartButton();
closeModal('cartModal');
} else {
console.error('Ошибка при сохранении заказа:', data.error);
}
})
.catch(error => {
console.error('Ошибка при отправке запроса:', error);
});
}
function clearCart() {
localStorage.removeItem('cart');
closeModal('cartModal');
updateCartButton();
}
window.onclick = function(event) {
if (event.target.className === 'modal') event.target.style.display = "none";
}
updateCartButton();
</script>
</body>
</html>
'''
@app.route('/product/<int:index>')
def product_detail(index):
data = load_data()
products = data['products']
try:
product = products[index]
except IndexError:
return "Продукт не найден", 404
detail_html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ product['name'] }} - Optomshop</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #f0f2f5, #e9ecef);
color: #2d3748;
line-height: 1.6;
margin: 0;
}
.container {
max-width: 1300px;
margin: 0 auto;
padding: 20px;
}
.product-details {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
background: #fff;
border-radius: 15px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.product-details h1 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 10px;
}
.product-images {
width: 100%;
max-width: 300px;
}
.main-image {
width: 100%;
aspect-ratio: 1;
background-color: #fff;
border-radius: 10px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
}
.main-image img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.thumbnails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.thumbnail {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
}
.thumbnail:hover {
transform: scale(1.1);
}
.product-info {
width: 100%;
text-align: center;
}
.product-info p {
margin: 8px 0;
}
.back-button {
display: inline-block;
padding: 10px 20px;
background-color: #3b82f6;
color: white;
text-decoration: none;
border-radius: 8px;
margin-top: 20px;
transition: background-color 0.3s;
}
.back-button:hover {
background-color: #2563eb;
}
#lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.lightbox-slider {
position: relative;
width: 90%;
max-width: 500px;
overflow: hidden;
}
.lightbox-images {
display: flex;
transition: transform 0.3s ease;
}
.lightbox-img {
width: 100%;
max-height: 80vh;
object-fit: contain;
flex-shrink: 0;
}
#lightbox .close {
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 3rem;
cursor: pointer;
transition: color 0.3s;
}
#lightbox .close:hover {
color: #ccc;
}
</style>
</head>
<body>
<div class="container">
<div class="product-details">
<h1>{{ product['name'] }}</h1>
<div class="product-images">
<div class="main-image">
{% if product.get('photos') and product['photos']|length > 0 %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
alt="{{ product['name'] }}">
{% else %}
<img src="https://via.placeholder.com/150" alt="No Image">
{% endif %}
</div>
<div class="thumbnails">
{% if product.get('photos') %}
{% for photo in product['photos'] %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
alt="{{ product['name'] }}"
class="thumbnail"
onclick="showLightbox({{ loop.index0 }})">
{% endfor %}
{% endif %}
</div>
</div>
<div class="product-info">
<p><strong>Артикул:</strong> {{ product.get('article', 'Не указан') }}</p>
<p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
<p><strong>Размеры:</strong> {{ product.get('size', 'Не указан') }}</p>
<p><strong>Цена:</strong> {{ product['price']|int }} сом</p>
<p><strong>Описание:</strong> {{ product['description'] }}</p>
<a href="/catalog" class="back-button">Назад к каталогу</a>
</div>
</div>
</div>
<div id="lightbox" onclick="closeLightbox(event)">
<span class="close" onclick="closeLightbox(event)">×</span>
<div class="lightbox-slider">
<div class="lightbox-images" id="lightboxImages">
{% if product.get('photos') %}
{% for photo in product['photos'] %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
class="lightbox-img"
alt="{{ product['name'] }}">
{% endfor %}
{% else %}
<img src="https://via.placeholder.com/150" class="lightbox-img" alt="No Image">
{% endif %}
</div>
</div>
</div>
<script>
const photos = [
{% if product.get('photos') %}
{% for photo in product['photos'] %}
"https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}",
{% endfor %}
{% else %}
"https://via.placeholder.com/150"
{% endif %}
];
let currentIndex = 0;
function showLightbox(index) {
currentIndex = index;
updateLightbox();
document.getElementById('lightbox').style.display = 'flex';
}
function closeLightbox(event) {
if (event.target.id === 'lightbox' || event.target.className === 'close') {
document.getElementById('lightbox').style.display = 'none';
}
}
function updateLightbox() {
const lightboxImages = document.getElementById('lightboxImages');
const offset = -currentIndex * 100;
lightboxImages.style.transform = `translateX(${offset}%)`;
}
// Swipe functionality for lightbox
if (photos.length > 1) {
const lightboxSlider = document.querySelector('.lightbox-slider');
let startX = 0;
let isDragging = false;
lightboxSlider.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isDragging = true;
});
lightboxSlider.addEventListener('touchmove', (e) => {
if (!isDragging) return;
const currentX = e.touches[0].clientX;
const diff = currentX - startX;
if (Math.abs(diff) > 50) {
e.preventDefault();
if (diff > 0 && currentIndex > 0) {
currentIndex--;
} else if (diff < 0 && currentIndex < photos.length - 1) {
currentIndex++;
}
updateLightbox();
isDragging = false;
}
});
lightboxSlider.addEventListener('touchend', () => {
isDragging = false;
});
}
</script>
</body>
</html>
'''
return render_template_string(detail_html, product=product, repo_id=REPO_ID)
@app.route('/save_order', methods=['POST'])
def save_order_route():
try:
order = request.get_json()
orders = load_orders()
orders.append(order)
save_orders(orders)
return jsonify({'success': True})
except Exception as e:
logging.error(f"Error saving order: {e}")
return jsonify({'success': False, 'error': str(e)})
@app.route('/admin', methods=['GET', 'POST'])
def admin():
data = load_data()
products = data['products']
categories = data['categories']
orders = load_orders()
if request.method == 'POST':
action = request.form.get('action')
if action == 'add_category':
category_name = request.form.get('category_name').strip().lower()
if category_name and not any(c['name'].lower() == category_name for c in categories):
categories.append({'name': category_name, 'subcategories': []})
save_data(data)
return redirect(url_for('admin'))
return "Ошибка: Категория уже существует или не указано название", 400
elif action == 'add_subcategory':
category_index = int(request.form.get('category_index'))
subcategory_name = request.form.get('subcategory_name').strip().lower()
if subcategory_name and subcategory_name not in [sub.lower() for sub in categories[category_index]['subcategories']]:
categories[category_index]['subcategories'].append(subcategory_name)
save_data(data)
return redirect(url_for('admin'))
return "Ошибка: Подкатегория уже существует или не указано название", 400
elif action == 'delete_category':
category_index = int(request.form.get('category_index'))
deleted_category = categories.pop(category_index)
category_name = deleted_category['name'].lower()
for product in products:
if product.get('category', '').lower() == category_name:
if 'category' in product:
del product['category']
if 'subcategory' in product:
del product['subcategory']
save_data(data)
return redirect(url_for('admin'))
elif action == 'delete_subcategory':
category_index = int(request.form.get('category_index'))
subcategory_index = int(request.form.get('subcategory_index'))
deleted_subcategory = categories[category_index]['subcategories'].pop(subcategory_index).lower()
for product in products:
if product.get('category', '').lower() == categories[category_index]['name'].lower() and product.get('subcategory', '').lower() == deleted_subcategory:
if 'subcategory' in product:
del product['subcategory']
save_data(data)
return redirect(url_for('admin'))
elif action == 'add':
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
size = request.form.get('size')
article = request.form.get('article')
category = request.form.get('category').strip().lower() if request.form.get('category') else None
subcategory = request.form.get('subcategory').strip().lower() if request.form.get('subcategory') else None
photos_files = request.files.getlist('photos')
colors = request.form.getlist('colors')
photos_list = []
if photos_files:
for photo in photos_files[:10]:
if photo and photo.filename:
photo_filename = secure_filename(photo.filename)
if not photo_filename:
return "Ошибка: Недопустимое имя файла", 400
uploads_dir = 'uploads'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
if not HF_TOKEN_WRITE:
return "Ошибка: HF_TOKEN_WRITE не установлен", 500
try:
api = HfApi()
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,
commit_message=f"Добавлено фото: {name}"
)
photos_list.append(photo_filename)
except Exception as e:
logging.error(f"Ошибка при загрузке фото: {e}")
return f"Ошибка при загрузке фото: {e}", 500
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
if not name or not price or not description:
return "Ошибка: Заполните все поля", 400
try:
price = int(float(price.replace(',', '.')))
except ValueError:
return "Ошибка: Неверный формат цены", 400
new_product = {
'name': name,
'price': price,
'description': description,
'size': size if size else 'Не указан',
'article': article if article else 'Не указан',
'category': category if category else None,
'subcategory': subcategory if subcategory and category else None,
'photos': photos_list,
'colors': colors if colors else []
}
products.append(new_product)
save_data(data)
return redirect(url_for('admin'))
elif action == 'edit':
index = int(request.form.get('index'))
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
size = request.form.get('size')
article = request.form.get('article')
category = request.form.get('category').strip().lower() if request.form.get('category') else None
subcategory = request.form.get('subcategory').strip().lower() if request.form.get('subcategory') else None
photos_files = request.files.getlist('photos')
colors = request.form.getlist('colors')
if photos_files and any(photo.filename for photo in photos_files):
new_photos_list = []
for photo in photos_files[:10]:
if photo and photo.filename:
photo_filename = secure_filename(photo.filename)
if not photo_filename:
return "Ошибка: Недопустимое имя файла", 400
uploads_dir = 'uploads'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
if not HF_TOKEN_WRITE:
return "Ошибка: HF_TOKEN_WRITE не установлен", 500
try:
api = HfApi()
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,
commit_message=f"Обновлено фото: {name}"
)
new_photos_list.append(photo_filename)
except Exception as e:
logging.error(f"Ошибка при загрузке фото: {e}")
return f"Ошибка при загрузке фото: {e}", 500
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
products[index]['photos'] = new_photos_list
try:
products[index]['price'] = int(float(price.replace(',', '.')))
except ValueError:
return "Ошибка: Неверный формат цены", 400
products[index]['name'] = name
products[index]['description'] = description
products[index]['size'] = size if size else 'Не указан'
products[index]['article'] = article if article else 'Не указан'
products[index]['category'] = category if category else None
products[index]['subcategory'] = subcategory if subcategory and category else None
products[index]['colors'] = colors if colors else []
save_data(data)
return redirect(url_for('admin'))
elif action == 'delete':
index = int(request.form.get('index'))
del products[index]
save_data(data)
return redirect(url_for('admin'))
elif action == 'download_order':
order_index = int(request.form.get('order_index'))
order = orders[order_index]
sorted_items = sorted(order['items'], key=lambda item: (item['name'], item['color']))
df = pd.DataFrame({
'Фото': [item.get('photo', '') for item in sorted_items],
'Наименование': [item['name'] for item in sorted_items],
'Количество': [item['quantity'] for item in sorted_items],
'Цвет': [item['color'] for item in sorted_items],
'Итоговая цена': [item['price'] * item['quantity'] for item in sorted_items]
})
total_row = pd.DataFrame({
'Фото': [''],
'Наименование': ['Итого'],
'Количество': [''],
'Цвет': [''],
'Итоговая цена': [order['total']]
})
df = pd.concat([df, total_row], ignore_index=True)
output = BytesIO()
writer = pd.ExcelWriter(output, engine='xlsxwriter')
df.to_excel(writer, sheet_name='Order', index=False)
workbook = writer.book
worksheet = writer.sheets['Order']
worksheet.set_column('A:A', 20)
worksheet.set_column('B:B', 30)
worksheet.set_column('C:C', 15)
worksheet.set_column('D:D', 15)
worksheet.set_column('E:E', 20)
text_format = workbook.add_format({
'border': 1,
'valign': 'vcenter',
'text_wrap': True
})
for i, item in enumerate(sorted_items):
worksheet.set_row(i + 1, 100)
for col in range(1, 5):
worksheet.write(i + 1, col, df.iloc[i, col], text_format)
if item.get('photo'):
image_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}"
try:
response = requests.get(image_url, timeout=10)
response.raise_for_status()
image_data = BytesIO(response.content)
if len(image_data.getvalue()) > 0:
worksheet.insert_image(i + 1, 0, image_url, {
'image_data': image_data,
'x_scale': 0.05,
'y_scale': 0.05,
'object_position': 2,
'x_offset': 5,
'y_offset': 5
})
else:
logging.warning(f"Empty image data for {image_url}")
except Exception as e:
logging.error(f"Error with image {image_url}: {e}")
writer.close()
output.seek(0)
timestamp = datetime.fromisoformat(order['timestamp'].replace('Z', '+00:00')).strftime('%Y%m%d_%H%M%S')
filename = f"order_{timestamp}.xlsx"
return send_file(
output,
as_attachment=True,
download_name=filename,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
elif action == 'delete_order':
order_index = int(request.form.get('order_index'))
del orders[order_index]
save_orders(orders)
return redirect(url_for('admin'))
return render_template_string(get_admin_template(), products=products, categories=categories, orders=orders, repo_id=REPO_ID)
def get_admin_template():
return '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель - Optomshop</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #f0f2f5, #e9ecef);
color: #2d3748;
line-height: 1.6;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1, h2 {
text-align: center;
color: #1a202c;
}
.form-section {
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.form-section h2 {
margin-top: 0;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
}
label {
font-weight: 500;
}
input, textarea, select {
width: 100%;
padding: 10px;
border: 1px solid #e2e8f0;
border-radius: 5px;
font-size: 1rem;
}
textarea {
resize: vertical;
}
button {
padding: 10px;
border: none;
border-radius: 5px;
background-color: #3b82f6;
color: white;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #2563eb;
}
.delete-button {
background-color: #ef4444;
}
.delete-button:hover {
background-color: #dc2626;
}
.add-color-btn {
background-color: #10b981;
}
.add-color-btn:hover {
background-color: #059669;
}
.remove-color-btn {
background-color: #ef4444;
margin-left: 10px;
}
.remove-color-btn:hover {
background-color: #dc2626;
}
.color-input-group {
display: flex;
align-items: center;
gap: 10px;
}
.category-list, .order-list, .product-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.category-item, .order-item, .product-item {
background: #fff;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.subcategory-list {
margin-top: 10px;
padding-left: 20px;
}
.subcategory-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 0;
}
.order-item {
display: flex;
flex-direction: column;
gap: 10px;
}
.order-details {
margin-left: 20px;
}
.product-item {
display: flex;
flex-direction: column;
gap: 10px;
}
details {
margin-top: 10px;
}
summary {
cursor: pointer;
font-weight: 500;
color: #3b82f6;
}
.edit-form {
margin-top: 10px;
padding: 10px;
border: 1px solid #e2e8f0;
border-radius: 5px;
}
#product-search {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #e2e8f0;
border-radius: 5px;
font-size: 1rem;
}
</style>
</head>
<body>
<div class="container">
<h1>Админ-панель Optomshop</h1>
<div class="form-section">
<h2>Управление категориями</h2>
<form method="POST">
<input type="hidden" name="action" value="add_category">
<label>Добавить категорию:</label>
<input type="text" name="category_name" placeholder="Название категории" required>
<button type="submit">Добавить</button>
</form>
<div class="category-list">
{% for category in categories %}
<div class="category-item">
<span>{{ category['name'] }}</span>
<form method="POST">
<input type="hidden" name="action" value="delete_category">
<input type="hidden" name="category_index" value="{{ loop.index0 }}">
<button type="submit" class="delete-button">Удалить</button>
</form>
</div>
<div class="subcategory-list">
{% for subcat in category['subcategories'] %}
<div class="subcategory-item">
<span>{{ subcat }}</span>
<form method="POST">
<input type="hidden" name="action" value="delete_subcategory">
<input type="hidden" name="category_index" value="{{ loop.index0 }}">
<input type="hidden" name="subcategory_index" value="{{ loop.index0 }}">
<button type="submit" class="delete-button">Удалить</button>
</form>
</div>
{% endfor %}
<form method="POST">
<input type="hidden" name="action" value="add_subcategory">
<input type="hidden" name="category_index" value="{{ loop.index0 }}">
<label>Добавить подкатегорию:</label>
<input type="text" name="subcategory_name" placeholder="Название подкатегории" required>
<button type="submit">Добавить</button>
</form>
</div>
{% endfor %}
</div>
</div>
<div class="form-section">
<h2>Добавить товар</h2>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="add">
<label>Название:</label>
<input type="text" name="name" required>
<label>Артикул:</label>
<input type="text" name="article">
<label>Цена:</label>
<input type="number" name="price" step="1" required>
<label>Описание:</label>
<textarea name="description" rows="4" required></textarea>
<label>Размеры:</label>
<input type="text" name="size">
<label>Категория:</label>
<select name="category" onchange="updateSubcategories(this)">
<option value="">Без категории</option>
{% for category in categories %}
<option value="{{ category['name'] }}">{{ category['name'] }}</option>
{% endfor %}
</select>
<label>Подкатегория:</label>
<select name="subcategory" id="subcategory-select">
<option value="">Без подкатегории</option>
</select>
<label>Фотографии (до 10):</label>
<input type="file" name="photos" accept="image/*" multiple>
<label>Цвета или расцветки:</label>
<div id="color-inputs"></div>
<button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
<button type="submit">Добавить товар</button>
</form>
</div>
<div class="form-section">
<h2>Список товаров</h2>
<input type="text" id="product-search" placeholder="Поиск товаров...">
<div class="product-list" id="product-list">
{% for product in products %}
<div class="product-item"
data-name="{{ product['name']|lower }}"
data-description="{{ product['description']|lower }}">
<h3>{{ product['name'] }}</h3>
<p><strong>Артикул:</strong> {{ product.get('article', 'Не указан') }}</p>
<p><strong>Размеры:</strong> {{ product.get('size', 'Не указан') }}</p>
<p><strong>Категория:</strong> {{ product.get('category', 'Не указана') }}</p>
<p><strong>Подкатегория:</strong> {{ product.get('subcategory', 'Не указана') }}</p>
<p><strong>Цена:</strong> {{ product['price']|int }} сом</p>
<p><strong>Описание:</strong> {{ product['description'] }}</p>
<p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
{% if product.get('photos') and product['photos']|length > 0 %}
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
{% for photo in product['photos'] %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
alt="{{ product['name'] }}"
style="max-width: 100px; border-radius: 10px;">
{% endfor %}
</div>
{% endif %}
<details>
<summary>Редактировать</summary>
<form method="POST" enctype="multipart/form-data" class="edit-form">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="index" value="{{ loop.index0 }}">
<label>Название:</label>
<input type="text" name="name" value="{{ product['name'] }}" required>
<label>Артикул:</label>
<input type="text" name="article" value="{{ product.get('article', '') }}">
<label>Цена:</label>
<input type="number" name="price" step="1" value="{{ product['price']|int }}" required>
<label>Описание:</label>
<textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
<label>Размеры:</label>
<input type="text" name="size" value="{{ product.get('size', '') }}">
<label>Категория:</label>
<select name="category" onchange="updateSubcategories(this, 'edit-subcategory-{{ loop.index0 }}')">
<option value="">Без категории</option>
{% for category in categories %}
<option value="{{ category['name'] }}" {% if product.get('category') == category['name'] %}selected{% endif %}>{{ category['name'] }}</option>
{% endfor %}
</select>
<label>Подкатегория:</label>
<select name="subcategory" id="edit-subcategory-{{ loop.index0 }}">
<option value="">Без подкатегории</option>
{% for category in categories %}
{% if category['name'] == product.get('category') %}
{% for subcat in category['subcategories'] %}
<option value="{{ subcat }}" {% if product.get('subcategory') == subcat %}selected{% endif %}>{{ subcat }}</option>
{% endfor %}
{% endif %}
{% endfor %}
</select>
<label>Фотографии (до 10):</label>
<input type="file" name="photos" accept="image/*" multiple>
<label>Цвета или расцветки:</label>
<div id="edit-color-inputs-{{ loop.index0 }}">
{% for color in product.get('colors', []) %}
<div class="color-input-group">
<input type="text" name="colors" value="{{ color }}">
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
</div>
{% endfor %}
</div>
<button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
<button type="submit">Сохранить</button>
</form>
</details>
<form method="POST">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="index" value="{{ loop.index0 }}">
<button type="submit" class="delete-button">Удалить</button>
</form>
</div>
{% endfor %}
</div>
</div>
<div class="form-section">
<h2>Список заказов</h2>
<div class="order-list">
{% for order in orders %}
<div class="order-item">
<p><strong>Дата:</strong> {{ order['timestamp'] }}</p>
<p><strong>Итого:</strong> {{ order['total'] }} сом</p>
<div class="order-details">
{% for item in order['items'] %}
<p>{{ item['name'] }} - {{ item['price'] }} сом × {{ item['quantity'] }} (Цвет: {{ item['color'] }})</p>
{% endfor %}
</div>
<form method="POST">
<input type="hidden" name="action" value="download_order">
<input type="hidden" name="order_index" value="{{ loop.index0 }}">
<button type="submit">Скачать в Excel</button>
</form>
<form method="POST">
<input type="hidden" name="action" value="delete_order">
<input type="hidden" name="order_index" value="{{ loop.index0 }}">
<button type="submit" class="delete-button">Удалить</button>
</form>
</div>
{% endfor %}
</div>
</div>
</div>
<script>
const categories = {{ categories|tojson }};
function addColorInput(containerId = 'color-inputs') {
const container = document.getElementById(containerId);
const newInputGroup = document.createElement('div');
newInputGroup.className = 'color-input-group';
newInputGroup.innerHTML = `
<input type="text" name="colors" placeholder="Например: Красный">
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)">Удалить</button>
`;
container.appendChild(newInputGroup);
}
function removeColorInput(button) {
const colorInputGroup = button.parentNode;
colorInputGroup.remove();
}
function updateSubcategories(select, targetId = 'subcategory-select') {
const categoryName = select.value;
const subcategorySelect = document.getElementById(targetId);
subcategorySelect.innerHTML = '<option value="">Без подкатегории</option>';
if (categoryName) {
const category = categories.find(cat => cat.name === categoryName);
if (category && category.subcategories) {
category.subcategories.forEach(subcat => {
const option = document.createElement('option');
option.value = subcat;
option.text = subcat;
subcategorySelect.appendChild(option);
});
}
}
}
document.getElementById('product-search').addEventListener('input', filterProducts);
function filterProducts() {
const searchTerm = document.getElementById('product-search').value.toLowerCase();
document.querySelectorAll('.product-item').forEach(product => {
const name = product.dataset.name;
const description = product.dataset.description;
const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
product.style.display = matchesSearch ? 'block' : 'none';
});
}
</script>
</body>
</html>
'''
@app.route('/backup', methods=['POST'])
def backup():
upload_db_to_hf()
return "Резервная копия создана.", 200
@app.route('/download', methods=['GET'])
def download():
download_db_from_hf()
return "База данных скачана.", 200
if __name__ == '__main__':
backup_thread = threading.Thread(target=periodic_backup)
backup_thread.daemon = True
backup_thread.start()
app.run(debug=True, host='0.0.0.0', port=7860)