Vid / app.py
Eluza133's picture
Update app.py
f93ccf6 verified
from flask import Flask, render_template_string, request, redirect, url_for, session, flash
import json
import os
import logging
import threading
import time
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download
from werkzeug.utils import secure_filename
import random
app = Flask(__name__)
app.secret_key = 'supersecretkey' # Замените на безопасный ключ в продакшене
DATA_FILE = 'data_ostenhost.json'
REPO_ID = "Eluza133/w1f9"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен с правами записи
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
# Настройка логирования
logging.basicConfig(level=logging.DEBUG)
# Функции работы с базой данных
def load_data():
try:
download_db_from_hf()
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
logging.warning("Данные не в формате dict, инициализация пустой базы")
return {'posts': [], 'users': {}, 'pending_organizations': [], 'orders': {}, 'user_orders': {}}
if 'posts' not in data:
data['posts'] = []
if 'users' not in data:
data['users'] = {}
if 'pending_organizations' not in data:
data['pending_organizations'] = []
if 'orders' not in data:
data['orders'] = {}
if 'user_orders' not in data:
data['user_orders'] = {}
logging.info("Данные успешно загружены")
return data
except Exception as e:
logging.error(f"Ошибка загрузки данных: {e}")
return {'posts': [], 'users': {}, 'pending_organizations': [], 'orders': {}, 'user_orders': {}}
def save_data(data):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
upload_db_to_hf()
logging.info("Данные сохранены и загружены на HF")
except Exception as e:
logging.error(f"Ошибка сохранения данных: {e}")
raise
def upload_db_to_hf():
try:
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"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info("База данных загружена на Hugging Face")
except Exception as e:
logging.error(f"Ошибка загрузки базы: {e}")
def download_db_from_hf():
try:
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("База данных скачана с Hugging Face")
except Exception as e:
logging.error(f"Ошибка скачивания базы: {e}")
if not os.path.exists(DATA_FILE):
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump({'posts': [], 'users': {}, 'pending_organizations': [], 'orders': {}, 'user_orders': {}}, f)
def periodic_backup():
while True:
upload_db_to_hf()
time.sleep(800)
# Обновленный базовый стиль
BASE_STYLE = '''
:root {
--primary: #6b48ff;
--secondary: #ff4785;
--background-light: #f5f7ff;
--background-dark: #1a1a2e;
--card-bg-light: rgba(255, 255, 255, 0.95);
--card-bg-dark: rgba(40, 40, 60, 0.95);
--text-light: #1a1a2e;
--text-dark: #e0e0e0;
--shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
--glass-bg: rgba(255, 255, 255, 0.15);
--transition: all 0.4s ease;
--cart-btn: #10b981;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--background-light);
color: var(--text-light);
line-height: 1.6;
transition: var(--transition);
overflow-x: hidden;
}
body.dark {
background: var(--background-dark);
color: var(--text-dark);
}
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100%;
background: var(--glass-bg);
backdrop-filter: blur(20px);
padding: 30px;
box-shadow: var(--shadow);
z-index: 1000;
transition: var(--transition);
}
.sidebar-header {
margin-bottom: 40px;
}
.nav-brand {
font-size: 2em;
font-weight: 700;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.nav-links {
display: flex;
flex-direction: column;
gap: 20px;
}
.nav-link {
display: flex;
align-items: center;
gap: 15px;
padding: 18px;
background: var(--card-bg-light);
color: var(--text-light);
text-decoration: none;
border-radius: 15px;
font-size: 1.1em;
transition: var(--transition);
position: relative;
}
body.dark .nav-link {
background: var(--card-bg-dark);
color: var(--text-dark);
}
.nav-link:hover {
transform: translateX(15px);
background: var(--primary);
color: white;
}
.logout-btn {
background: var(--secondary);
color: white;
}
.logout-btn:hover {
background: #e63970;
}
.order-count {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
background: var(--secondary);
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9em;
}
.menu-btn {
display: none;
font-size: 32px;
background: var(--glass-bg);
border: none;
color: var(--primary);
cursor: pointer;
position: fixed;
top: 20px;
left: 20px;
z-index: 1001;
padding: 12px;
border-radius: 50%;
box-shadow: var(--shadow);
}
.container {
margin: 30px auto 30px 330px;
max-width: 1200px;
padding: 30px;
transition: var(--transition);
}
.btn {
padding: 14px 28px;
background: var(--primary);
color: white;
border: none;
border-radius: 15px;
cursor: pointer;
font-size: 1.1em;
transition: var(--transition);
display: inline-block;
margin: 10px 0;
}
.btn:hover {
transform: scale(1.08);
background: #5439cc;
}
.cart-btn {
background: var(--cart-btn);
}
.cart-btn:hover {
background: #0d9f6e;
}
input, textarea, select {
width: 100%;
padding: 14px;
margin: 15px 0;
border: 2px solid rgba(0, 0, 0, 0.1);
border-radius: 15px;
background: var(--glass-bg);
color: var(--text-light);
font-size: 1.1em;
transition: var(--transition);
}
body.dark input, body.dark textarea, body.dark select {
border: 2px solid rgba(255, 255, 255, 0.1);
color: var(--text-dark);
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
background: rgba(255, 255, 255, 0.2);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal img {
max-width: 95%;
max-height: 95%;
object-fit: contain;
border-radius: 15px;
box-shadow: var(--shadow);
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
background: var(--glass-bg);
border: none;
padding: 12px;
border-radius: 50%;
cursor: pointer;
font-size: 24px;
box-shadow: var(--shadow);
transition: var(--transition);
}
.cart-float {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--cart-btn);
color: white;
border-radius: 50%;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
cursor: pointer;
z-index: 1000;
transition: var(--transition);
}
.cart-float:hover {
transform: scale(1.1);
background: #0d9f6e;
}
.cart-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--card-bg-light);
padding: 30px;
border-radius: 25px;
box-shadow: var(--shadow);
z-index: 2000;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
body.dark .cart-modal {
background: var(--card-bg-dark);
}
.cart-modal h2 {
font-size: 1.8em;
margin-bottom: 20px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.cart-item {
background: var(--glass-bg);
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
}
.cart-item h3 {
font-size: 1.2em;
margin-bottom: 10px;
}
.cart-total {
font-size: 1.2em;
font-weight: 600;
margin-top: 20px;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
width: 280px;
}
.sidebar.active {
transform: translateX(0);
}
.menu-btn {
display: block;
}
.container {
margin: 80px 15px 30px 15px;
max-width: calc(100% - 30px);
}
.theme-toggle {
top: 80px;
right: 15px;
}
.btn, input, textarea, select {
font-size: 1em;
padding: 12px;
}
.cart-float {
bottom: 15px;
right: 15px;
width: 50px;
height: 50px;
}
}
'''
NAV_HTML = '''
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<span class="nav-brand">OstenHost</span>
</div>
<nav class="nav-links">
<a href="{{ url_for('feed') }}" class="nav-link"><span>📜</span> Лента</a>
{% if is_authenticated %}
<a href="{{ url_for('profile') }}" class="nav-link"><span>👤</span> Профиль ({{ username }})</a>
<a href="{{ url_for('upload') }}" class="nav-link"><span>⬆️</span> Загрузить</a>
{% if user_type == 'seller' and verified %}
<a href="{{ url_for('seller_orders') }}" class="nav-link"><span>🛒</span> Заказы {% if order_count > 0 %}<span class="order-count">{{ order_count }}</span>{% endif %}</a>
{% endif %}
<a href="{{ url_for('user_orders') }}" class="nav-link"><span>📦</span> Мои заказы</a>
<a href="{{ url_for('logout') }}" class="nav-link logout-btn"><span>🚪</span> Выйти</a>
{% else %}
<a href="{{ url_for('login') }}" class="nav-link"><span>🔑</span> Войти</a>
<a href="{{ url_for('register') }}" class="nav-link"><span>✨</span> Регистрация</a>
{% endif %}
</nav>
</aside>
'''
# Регистрация
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
logging.debug(f"Получен POST-запрос: {request.form}")
username = request.form.get('username')
password = request.form.get('password')
user_type = request.form.get('user_type')
phone = request.form.get('phone')
address = request.form.get('address')
data = load_data()
if not username or not password or not phone or (user_type == 'buyer' and not address):
flash('Заполните все обязательные поля!', 'error')
logging.debug("Не все обязательные поля заполнены")
return redirect(url_for('register'))
if username in data['users']:
flash('Пользователь уже существует!', 'error')
logging.debug(f"Пользователь {username} уже существует")
return redirect(url_for('register'))
if 'register_seller' in request.form:
logging.debug("Нажата кнопка регистрации продавца")
org_name = request.form.get('org_name')
org_phone = request.form.get('org_phone')
is_online = request.form.get('is_online') == 'on'
org_address = request.form.get('org_address') if not is_online else None
if not org_name or not org_phone:
flash('Укажите название организации и рабочий номер!', 'error')
logging.debug("Не указаны org_name или org_phone")
return redirect(url_for('register'))
data['users'][username] = {
'password': password,
'bio': '',
'link': '',
'avatar': None,
'type': 'seller',
'verified': False,
'phone': phone,
'org_phone': org_phone,
'org_address': org_address,
'is_online': is_online
}
data['pending_organizations'].append({
'username': username,
'org_name': org_name,
'org_phone': org_phone,
'org_address': org_address,
'is_online': is_online,
'submitted_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
save_data(data)
flash('Ваша заявка принята, мы с вами свяжемся в течение 2 суток', 'success')
logging.debug(f"Продавец {username} зарегистрирован и отправлен на верификацию")
return redirect(url_for('login'))
elif 'register_buyer' in request.form:
logging.debug("Нажата кнопка регистрации покупателя")
data['users'][username] = {
'password': password,
'bio': '',
'link': '',
'avatar': None,
'type': 'buyer',
'verified': True,
'phone': phone,
'address': address
}
save_data(data)
flash('Регистрация успешна! Войдите в систему.', 'success')
logging.debug(f"Покупатель {username} зарегистрирован")
return redirect(url_for('login'))
else:
flash('Неизвестная ошибка при выборе типа пользователя!', 'error')
logging.debug("Ни одна из кнопок регистрации не была нажата")
return redirect(url_for('register'))
is_authenticated = 'username' in session
username = session.get('username', None)
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Регистрация</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 650px;
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 50px;
border-radius: 25px;
box-shadow: var(--shadow);
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.2em;
font-weight: 600;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.flash {
text-align: center;
margin-bottom: 20px;
font-size: 1.1em;
font-weight: 500;
}
.flash.success {
color: #10b981;
}
.flash.error {
color: var(--secondary);
}
.link {
text-align: center;
margin-top: 25px;
color: var(--primary);
font-size: 1.1em;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.toggle-form {
margin-bottom: 20px;
text-align: center;
}
.toggle-form label {
margin: 0 20px;
font-size: 1.2em;
}
#seller-fields {
display: none;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Регистрация</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="toggle-form">
<label><input type="radio" name="user_type" value="buyer" checked onclick="toggleSellerFields(false)"> Покупатель</label>
<label><input type="radio" name="user_type" value="seller" onclick="toggleSellerFields(true)"> Продавец</label>
</div>
<form method="POST">
<input type="text" name="username" placeholder="Логин" required>
<input type="password" name="password" placeholder="Пароль" required>
<input type="text" name="phone" placeholder="Номер телефона (с кодом, например, +996)" required>
<input type="text" name="address" placeholder="Адрес доставки" id="address_field">
<div id="seller-fields">
<input type="text" name="org_name" placeholder="Название организации">
<input type="text" name="org_phone" placeholder="Рабочий номер организации">
<label><input type="checkbox" name="is_online" onclick="toggleAddress()"> Организация онлайн</label>
<input type="text" name="org_address" placeholder="Адрес организации" id="org_address">
</div>
<button type="submit" name="register_buyer" class="btn">Зарегистрироваться как покупатель</button>
<button type="submit" name="register_seller" class="btn">Зарегистрироваться как продавец</button>
</form>
<p class="link"><a href="{{ url_for('login') }}">Уже есть аккаунт? Войти</a></p>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleSellerFields(show) {
document.getElementById('seller-fields').style.display = show ? 'block' : 'none';
document.getElementById('address_field').required = !show;
}
function toggleAddress() {
const isOnline = document.querySelector('input[name="is_online"]').checked;
document.getElementById('org_address').disabled = isOnline;
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html, is_authenticated=is_authenticated, username=username)
# Авторизация
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
data = load_data()
if username in data['users'] and data['users'][username]['password'] == password:
session['username'] = username
session.permanent = True
return redirect(url_for('feed'))
flash('Неверный логин или пароль!', 'error')
return redirect(url_for('login'))
is_authenticated = 'username' in session
username = session.get('username', None)
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 550px;
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 50px;
border-radius: 25px;
box-shadow: var(--shadow);
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.2em;
font-weight: 600;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.flash {
text-align: center;
margin-bottom: 20px;
font-size: 1.1em;
font-weight: 500;
}
.flash.success {
color: #10b981;
}
.flash.error {
color: var(--secondary);
}
.link {
text-align: center;
margin-top: 25px;
color: var(--primary);
font-size: 1.1em;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Вход</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<input type="text" name="username" placeholder="Логин" required>
<input type="password" name="password" placeholder="Пароль" required>
<button type="submit" class="btn">Войти</button>
</form>
<p class="link"><a href="{{ url_for('register') }}">Нет аккаунта? Зарегистрироваться</a></p>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html, is_authenticated=is_authenticated, username=username)
# Выход
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('feed'))
# Лента
@app.route('/', methods=['GET', 'POST'])
def feed():
data = load_data()
posts = sorted(data.get('posts', []), key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
is_authenticated = 'username' in session
username = session.get('username', None)
user_type = data['users'].get(username, {}).get('type') if username else None
verified = data['users'].get(username, {}).get('verified', False) if username else False
order_count = len(data['orders'].get(username, [])) if user_type == 'seller' and verified else 0
search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' else request.args.get('search', '').strip().lower()
if search_query:
posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()]
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Лента</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
h1 {
font-size: 2.8em;
font-weight: 700;
margin-bottom: 40px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.search-container {
max-width: 600px;
margin: 0 auto 40px;
position: relative;
}
.search-input {
padding-right: 60px;
background: var(--card-bg-light);
border: none;
box-shadow: var(--shadow);
}
body.dark .search-input {
background: var(--card-bg-dark);
}
.search-btn {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
background: var(--primary);
padding: 10px 20px;
border-radius: 12px;
font-size: 1em;
box-shadow: none;
}
.search-btn:hover {
background: #5439cc;
}
.post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 30px;
}
.post-item {
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 25px;
border-radius: 20px;
box-shadow: var(--shadow);
transition: var(--transition);
text-decoration: none;
color: var(--text-light);
}
body.dark .post-item {
background: var(--card-bg-dark);
color: var(--text-dark);
}
.post-item:hover {
transform: translateY(-15px);
}
.post-preview {
width: 100%;
height: 240px;
object-fit: cover;
border-radius: 15px;
margin-bottom: 20px;
}
.post-item h2 {
font-size: 1.5em;
margin-bottom: 15px;
}
.post-item p {
margin-bottom: 15px;
font-size: 1.1em;
}
.stats {
font-size: 1em;
color: rgba(0, 0, 0, 0.6);
}
body.dark .stats {
color: rgba(255, 255, 255, 0.6);
}
.username-link {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.username-link:hover {
text-decoration: underline;
}
.price {
font-weight: 600;
color: var(--primary);
}
.quantity-input {
width: 60px;
display: inline-block;
margin-right: 10px;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<h1>Лента публикаций</h1>
<div class="search-container">
<form method="POST">
<input type="text" name="search" class="search-input" placeholder="Поиск по названию или описанию" value="{{ search_query }}">
<button type="submit" class="search-btn">Искать</button>
</form>
</div>
<div class="post-grid">
{% for post in posts %}
<div class="post-item">
<a href="{{ url_for('post_page', post_id=post['id']) }}">
<video class="post-preview" preload="metadata" muted>
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/videos/{{ post['filename'] }}" type="video/mp4">
</video>
<h2>{{ post['title'] }}</h2>
</a>
<p>{{ post['description'] }}</p>
<p class="price">Цена: {{ post['price'] }} {{ post['currency'] }}</p>
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p>
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p>
{% if is_authenticated and user_type == 'buyer' and verified %}
<input type="number" class="quantity-input" id="quantity_{{ post['id'] }}" min="1" value="1">
<button class="btn cart-btn" onclick="addToCart('{{ post['id'] }}', '{{ post['title'] }}', '{{ post['price'] }}', '{{ post['currency'] }}', '{{ post['uploader'] }}')">В корзину</button>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<div class="modal" id="imageModal" onclick="closeModal(event)">
<img id="modalImage" src="">
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function openModal(src, event) {
event.preventDefault();
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
modal.style.display = 'flex';
modalImg.src = src;
}
function closeModal(event) {
if (event.target.tagName !== 'IMG') {
document.getElementById('imageModal').style.display = 'none';
}
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function addToCart(postId, title, price, currency, uploader) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
const quantity = parseInt(document.getElementById('quantity_' + postId).value) || 1;
const existingItem = cart.find(item => item.postId === postId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({ postId, title, price: parseFloat(price), currency, uploader, quantity });
}
localStorage.setItem('cart', JSON.stringify(cart));
document.getElementById('cartFloat').style.display = 'block';
alert('Добавлено в корзину!');
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const div = document.createElement('div');
div.className = 'cart-item';
const itemTotal = item.price * item.quantity;
total += itemTotal;
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
const videos = document.querySelectorAll('.post-preview');
videos.forEach(video => {
video.addEventListener('loadedmetadata', () => {
video.currentTime = Math.random() * video.duration;
});
});
};
</script>
</body>
</html>
'''
return render_template_string(html, posts=posts, is_authenticated=is_authenticated, username=username, repo_id=REPO_ID, search_query=search_query, user_type=user_type, verified=verified, order_count=order_count)
# Страница публикации
@app.route('/post/<post_id>', methods=['GET', 'POST'])
def post_page(post_id):
data = load_data()
post = next((p for p in data['posts'] if p['id'] == post_id), None)
if not post:
return "Публикация не найдена", 404
is_authenticated = 'username' in session
username = session.get('username', None)
user_type = data['users'].get(username, {}).get('type') if username else None
verified = data['users'].get(username, {}).get('verified', False) if username else False
order_count = len(data['orders'].get(username, [])) if user_type == 'seller' and verified else 0
post['views'] = post.get('views', 0) + 1
save_data(data)
if request.method == 'POST' and is_authenticated:
if 'like' in request.form:
if username not in post.get('likes', []):
post['likes'] = post.get('likes', []) + [username]
else:
post['likes'] = [user for user in post.get('likes', []) if user != username]
save_data(data)
elif 'comment' in request.form:
comment_text = request.form.get('comment')
if comment_text:
post['comments'] = post.get('comments', []) + [{'user': username, 'text': comment_text, 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]
save_data(data)
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ post['title'] }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 950px;
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 50px;
border-radius: 25px;
box-shadow: var(--shadow);
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.5em;
font-weight: 700;
margin-bottom: 30px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
video {
width: 100%;
max-height: 500px;
object-fit: cover;
border-radius: 15px;
box-shadow: var(--shadow);
margin-bottom: 30px;
}
p {
font-size: 1.2em;
margin-bottom: 20px;
}
.like-btn.liked {
background: var(--secondary);
}
.like-btn.liked:hover {
background: #e63970;
}
.comment-section {
margin-top: 40px;
}
.comment {
background: var(--glass-bg);
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
font-size: 1.1em;
}
.username-link {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.username-link:hover {
text-decoration: underline;
}
.price {
font-weight: 600;
color: var(--primary);
}
.quantity-input {
width: 60px;
display: inline-block;
margin-right: 10px;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<h1>{{ post['title'] }}</h1>
<video controls>
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/videos/{{ post['filename'] }}" type="video/mp4">
</video>
<p>{{ post['description'] }}</p>
<p class="price">Цена: {{ post['price'] }} {{ post['currency'] }}</p>
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p>
<p>Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p>
{% if is_authenticated %}
<form method="POST" style="display: inline;">
<button type="submit" name="like" class="btn like-btn {% if username in post['likes'] %}liked{% endif %}">
{% if username in post['likes'] %}Убрать лайк{% else %}Лайк{% endif %}
</button>
</form>
{% if user_type == 'buyer' and verified %}
<input type="number" class="quantity-input" id="quantity_{{ post['id'] }}" min="1" value="1">
<button class="btn cart-btn" onclick="addToCart('{{ post['id'] }}', '{{ post['title'] }}', '{{ post['price'] }}', '{{ post['currency'] }}', '{{ post['uploader'] }}')">В корзину</button>
{% endif %}
<form method="POST" class="comment-section">
<textarea name="comment" placeholder="Оставьте комментарий" rows="4"></textarea>
<button type="submit" class="btn">Отправить</button>
</form>
{% endif %}
<h3 style="margin-top: 40px; font-size: 1.8em;">Комментарии</h3>
{% for comment in post.get('comments', []) %}
<div class="comment">
<p><strong><a href="{{ url_for('user_profile', username=comment['user']) }}" class="username-link">{{ comment['user'] }}</a></strong> ({{ comment['date'] }}): {{ comment['text'] }}</p>
</div>
{% endfor %}
<a href="{{ url_for('feed') }}" class="btn">Назад к ленте</a>
{% if not is_authenticated %}
<p style="margin-top: 20px;"><a href="{{ url_for('login') }}">Войдите</a>, чтобы ставить лайки и комментировать.</p>
{% endif %}
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function addToCart(postId, title, price, currency, uploader) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
const quantity = parseInt(document.getElementById('quantity_' + postId).value) || 1;
const existingItem = cart.find(item => item.postId === postId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({ postId, title, price: parseFloat(price), currency, uploader, quantity });
}
localStorage.setItem('cart', JSON.stringify(cart));
document.getElementById('cartFloat').style.display = 'block';
alert('Добавлено в корзину!');
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const div = document.createElement('div');
div.className = 'cart-item';
const itemTotal = item.price * item.quantity;
total += itemTotal;
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
};
</script>
</body>
</html>
'''
return render_template_string(html, post=post, repo_id=REPO_ID, is_authenticated=is_authenticated, username=username, user_type=user_type, verified=verified, order_count=order_count)
# Профиль пользователя
@app.route('/profile', methods=['GET', 'POST'])
def profile():
if 'username' not in session:
flash('Войдите, чтобы просмотреть профиль!', 'error')
return redirect(url_for('login'))
data = load_data()
username = session['username']
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
is_authenticated = 'username' in session
total_views = sum(post.get('views', 0) for post in user_posts)
total_likes = sum(len(post.get('likes', [])) for post in user_posts)
user_data = data['users'].get(username, {})
bio = user_data.get('bio', '')
link = user_data.get('link', '')
avatar = user_data.get('avatar', None)
user_type = user_data.get('type', 'buyer')
verified = user_data.get('verified', False)
phone = user_data.get('phone', '')
address = user_data.get('address', '')
org_phone = user_data.get('org_phone', '')
org_address = user_data.get('org_address', '')
is_online = user_data.get('is_online', False)
order_count = len(data['orders'].get(username, [])) if user_type == 'seller' and verified else 0
if request.method == 'POST':
if 'delete_post' in request.form:
post_id = request.form.get('post_id')
if post_id:
data['posts'] = [p for p in data['posts'] if p['id'] != post_id or p['uploader'] != username]
save_data(data)
return redirect(url_for('profile'))
elif 'update_profile' in request.form:
bio = request.form.get('bio', '')
link = request.form.get('link', '')
phone = request.form.get('phone', '')
address = request.form.get('address', '') if user_type == 'buyer' else address
org_phone = request.form.get('org_phone', '') if user_type == 'seller' else org_phone
org_address = request.form.get('org_address', '') if user_type == 'seller' and not is_online else None
avatar_file = request.files.get('avatar')
if avatar_file and avatar_file.filename:
filename = secure_filename(avatar_file.filename)
temp_path = os.path.join('uploads', filename)
os.makedirs('uploads', exist_ok=True)
avatar_file.save(temp_path)
api = HfApi()
avatar_path = f"avatars/{username}/{filename}"
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=avatar_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Добавлен аватар для {username}"
)
data['users'][username]['avatar'] = avatar_path
if os.path.exists(temp_path):
os.remove(temp_path)
data['users'][username]['bio'] = bio
data['users'][username]['link'] = link
data['users'][username]['phone'] = phone
if user_type == 'buyer':
data['users'][username]['address'] = address
if user_type == 'seller' and verified:
data['users'][username]['org_phone'] = org_phone
data['users'][username]['org_address'] = org_address
save_data(data)
return redirect(url_for('profile'))
elif 'checkout' in request.form:
cart = json.loads(request.form.get('cart_data', '[]'))
if cart:
if not phone or (user_type == 'buyer' and not address):
flash('Укажите номер телефона и адрес доставки в профиле перед оформлением заказа!', 'error')
return redirect(url_for('profile'))
for item in cart:
seller = item['uploader']
order_id = f"order_{int(time.time())}_{random.randint(1, 1000)}"
order_data = {
'order_id': order_id,
'buyer': username,
'post_id': item['postId'],
'title': item['title'],
'price': item['price'],
'currency': item['currency'],
'quantity': item['quantity'],
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'status': 'pending',
'buyer_phone': phone,
'buyer_address': address if user_type == 'buyer' else None
}
if seller not in data['orders']:
data['orders'][seller] = []
data['orders'][seller].append(order_data)
if username not in data['user_orders']:
data['user_orders'][username] = []
data['user_orders'][username].append(order_data)
save_data(data)
flash('Заказ успешно оформлен!', 'success')
return redirect(url_for('profile'))
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Профиль - {{ username }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 950px;
}
.profile-header {
display: flex;
align-items: center;
gap: 30px;
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 40px;
border-radius: 25px;
box-shadow: var(--shadow);
margin-bottom: 40px;
}
body.dark .profile-header {
background: var(--card-bg-dark);
}
.avatar {
width: 140px;
height: 140px;
border-radius: 50%;
object-fit: cover;
box-shadow: var(--shadow);
}
.profile-info h1 {
font-size: 2.5em;
font-weight: 700;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
margin-bottom: 20px;
}
.profile-info p {
font-size: 1.2em;
margin-bottom: 15px;
}
.post-grid, .cart-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 30px;
}
.post-item, .cart-item {
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 25px;
border-radius: 20px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .post-item, body.dark .cart-item {
background: var(--card-bg-dark);
}
.post-item:hover, .cart-item:hover {
transform: translateY(-15px);
}
.post-preview {
width: 100%;
height: 240px;
object-fit: cover;
border-radius: 15px;
margin-bottom: 20px;
}
.post-item h3, .cart-item h3 {
font-size: 1.5em;
margin-bottom: 15px;
}
.delete-btn {
background: var(--secondary);
}
.delete-btn:hover {
background: #e63970;
}
.share-btn {
background: #10b981;
}
.share-btn:hover {
background: #0d9f6e;
}
h2 {
font-size: 2em;
margin: 40px 0 20px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.price {
font-weight: 600;
color: var(--primary);
}
.remove-cart-btn {
background: #ef4444;
}
.remove-cart-btn:hover {
background: #dc2626;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<div class="profile-header">
{% if avatar %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Аватар" class="avatar">
{% endif %}
<div class="profile-info">
<h1>{{ username }}</h1>
<p>{{ bio }}</p>
{% if link %}
<p><a href="{{ link }}" target="_blank">{{ link }}</a></p>
{% endif %}
<p>Тип: {{ 'Продавец' if user_type == 'seller' else 'Покупатель' }} {% if user_type == 'seller' and not verified %}(На проверке){% endif %}</p>
<p>Телефон: {{ phone }}</p>
{% if user_type == 'buyer' %}
<p>Адрес доставки: {{ address }}</p>
{% endif %}
{% if user_type == 'seller' and verified %}
<p>Рабочий номер: {{ org_phone }}</p>
<p>Адрес организации: {{ org_address if org_address else 'Онлайн' }}</p>
{% endif %}
<p>Просмотров: {{ total_views }} | Лайков: {{ total_likes }}</p>
<button class="btn share-btn" onclick="copyProfileLink()">Поделиться</button>
</div>
</div>
<h2>Редактировать профиль</h2>
<form method="POST" enctype="multipart/form-data">
<textarea name="bio" placeholder="Описание профиля" rows="4">{{ bio }}</textarea>
<input type="text" name="link" placeholder="Ссылка" value="{{ link }}">
<input type="text" name="phone" placeholder="Номер телефона" value="{{ phone }}" required>
{% if user_type == 'buyer' %}
<input type="text" name="address" placeholder="Адрес доставки" value="{{ address }}" required>
{% endif %}
{% if user_type == 'seller' and verified %}
<input type="text" name="org_phone" placeholder="Рабочий номер" value="{{ org_phone }}" required>
{% if not is_online %}
<input type="text" name="org_address" placeholder="Адрес организации" value="{{ org_address }}">
{% endif %}
{% endif %}
<input type="file" name="avatar" accept="image/*">
<button type="submit" name="update_profile" class="btn">Сохранить</button>
</form>
{% if user_type == 'seller' and verified %}
<a href="{{ url_for('upload') }}" class="btn">Добавить видео</a>
{% endif %}
<h2>Корзина</h2>
<div class="cart-grid" id="cartGrid"></div>
<p class="cart-total" id="cartTotalProfile">Итого: 0</p>
<button class="btn" onclick="checkout()" style="margin-top: 20px;">Оформить заказ</button>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2>Ваши видео</h2>
<div class="post-grid">
{% if user_posts %}
{% for post in user_posts %}
<div class="post-item">
<a href="{{ url_for('post_page', post_id=post['id']) }}">
<video class="post-preview" preload="metadata" muted>
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/videos/{{ post['filename'] }}" type="video/mp4">
</video>
<h3>{{ post['title'] }}</h3>
</a>
<p>{{ post['description'] }}</p>
<p class="price">Цена: {{ post['price'] }} {{ post['currency'] }}</p>
<p>{{ post['upload_date'] }}</p>
<form method="POST">
<input type="hidden" name="post_id" value="{{ post['id'] }}">
<button type="submit" name="delete_post" class="btn delete-btn">Удалить</button>
</form>
</div>
{% endfor %}
{% else %}
<p style="font-size: 1.2em;">Вы пока не загрузили ни одного видео.</p>
{% endif %}
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function copyProfileLink() {
navigator.clipboard.writeText(window.location.href).then(() => {
alert('Ссылка скопирована!');
});
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
const cartGrid = document.getElementById('cartGrid');
const cartTotalProfile = document.getElementById('cartTotalProfile');
cartItems.innerHTML = '';
cartGrid.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div.cloneNode(true));
cartGrid.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
cartTotalProfile.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
renderCart();
const videos = document.querySelectorAll('.post-preview');
videos.forEach(video => {
video.addEventListener('loadedmetadata', () => {
video.currentTime = Math.random() * video.duration;
});
});
};
</script>
</body>
</html>
'''
return render_template_string(html, username=username, user_posts=user_posts, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated, user_type=user_type, verified=verified, phone=phone, address=address, org_phone=org_phone, org_address=org_address, is_online=is_online, order_count=order_count)
# Профиль другого пользователя
@app.route('/profile/<username>')
def user_profile(username):
data = load_data()
if username not in data['users']:
return "Пользователь не найден", 404
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
is_authenticated = 'username' in session
current_user = session.get('username')
total_views = sum(post.get('views', 0) for post in user_posts)
total_likes = sum(len(post.get('likes', [])) for post in user_posts)
user_data = data['users'].get(username, {})
bio = user_data.get('bio', '')
link = user_data.get('link', '')
avatar = user_data.get('avatar', None)
user_type = user_data.get('type', 'buyer')
verified = user_data.get('verified', False)
phone = user_data.get('phone', '')
address = user_data.get('address', '')
org_phone = user_data.get('org_phone', '')
org_address = user_data.get('org_address', '')
is_online = user_data.get('is_online', False)
order_count = len(data['orders'].get(current_user, [])) if data['users'].get(current_user, {}).get('type') == 'seller' and data['users'].get(current_user, {}).get('verified', False) else 0
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Профиль - {{ username }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 950px;
}
.profile-header {
display: flex;
align-items: center;
gap: 30px;
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 40px;
border-radius: 25px;
box-shadow: var(--shadow);
margin-bottom: 40px;
}
body.dark .profile-header {
background: var(--card-bg-dark);
}
.avatar {
width: 140px;
height: 140px;
border-radius: 50%;
object-fit: cover;
box-shadow: var(--shadow);
}
.profile-info h1 {
font-size: 2.5em;
font-weight: 700;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
margin-bottom: 20px;
}
.profile-info p {
font-size: 1.2em;
margin-bottom: 15px;
}
.post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 30px;
}
.post-item {
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 25px;
border-radius: 20px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .post-item {
background: var(--card-bg-dark);
}
.post-item:hover {
transform: translateY(-15px);
}
.post-preview {
width: 100%;
height: 240px;
object-fit: cover;
border-radius: 15px;
margin-bottom: 20px;
}
.post-item h3 {
font-size: 1.5em;
margin-bottom: 15px;
}
.share-btn {
background: #10b981;
}
.share-btn:hover {
background: #0d9f6e;
}
h2 {
font-size: 2em;
margin: 40px 0 20px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.price {
font-weight: 600;
color: var(--primary);
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<div class="profile-header">
{% if avatar %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Аватар" class="avatar">
{% endif %}
<div class="profile-info">
<h1>{{ username }}</h1>
<p>{{ bio }}</p>
{% if link %}
<p><a href="{{ link }}" target="_blank">{{ link }}</a></p>
{% endif %}
<p>Тип: {{ 'Продавец' if user_type == 'seller' else 'Покупатель' }} {% if user_type == 'seller' and not verified %}(На проверке){% endif %}</p>
<p>Телефон: {{ phone }}</p>
{% if user_type == 'buyer' %}
<p>Адрес доставки: {{ address }}</p>
{% endif %}
{% if user_type == 'seller' and verified %}
<p>Рабочий номер: {{ org_phone }}</p>
<p>Адрес организации: {{ org_address if org_address else 'Онлайн' }}</p>
{% endif %}
<p>Просмотров: {{ total_views }} | Лайков: {{ total_likes }}</p>
<button class="btn share-btn" onclick="copyProfileLink()">Поделиться</button>
</div>
</div>
<h2>Видео</h2>
<div class="post-grid">
{% if user_posts %}
{% for post in user_posts %}
<div class="post-item">
<a href="{{ url_for('post_page', post_id=post['id']) }}">
<video class="post-preview" preload="metadata" muted>
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/videos/{{ post['filename'] }}" type="video/mp4">
</video>
<h3>{{ post['title'] }}</h3>
</a>
<p>{{ post['description'] }}</p>
<p class="price">Цена: {{ post['price'] }} {{ post['currency'] }}</p>
<p>{{ post['upload_date'] }}</p>
</div>
{% endfor %}
{% else %}
<p style="font-size: 1.2em;">Этот пользователь пока не загрузил ни одного видео.</p>
{% endif %}
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function copyProfileLink() {
navigator.clipboard.writeText(window.location.href).then(() => {
alert('Ссылка скопирована!');
});
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
const videos = document.querySelectorAll('.post-preview');
videos.forEach(video => {
video.addEventListener('loadedmetadata', () => {
video.currentTime = Math.random() * video.duration;
});
});
};
</script>
</body>
</html>
'''
return render_template_string(html, username=username, user_posts=user_posts, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated, user_type=user_type, verified=verified, phone=phone, address=address, org_phone=org_phone, org_address=org_address, is_online=is_online, order_count=order_count)
# Загрузка контента
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if 'username' not in session:
flash('Войдите, чтобы загрузить контент!', 'error')
return redirect(url_for('login'))
data = load_data()
username = session['username']
user_data = data['users'].get(username, {})
if user_data.get('type') != 'seller' or not user_data.get('verified'):
flash('Только проверенные продавцы могут загружать видео!', 'error')
return redirect(url_for('profile'))
if request.method == 'POST':
title = request.form.get('title')
description = request.form.get('description')
price = request.form.get('price')
currency = request.form.get('currency')
file = request.files.get('file')
uploader = session['username']
if not title or not price or not currency or not file:
flash('Укажите название, цену, валюту и выберите видео!', 'error')
return redirect(url_for('upload'))
if not file.filename.lower().endswith(('.mp4', '.mov', '.avi')):
flash('Загружайте только видео файлы (mp4, mov, avi)!', 'error')
return redirect(url_for('upload'))
filename = secure_filename(file.filename)
temp_path = os.path.join('uploads', filename)
os.makedirs('uploads', exist_ok=True)
file.save(temp_path)
api = HfApi()
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"videos/{filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Добавлено видео: {title} пользователем {uploader}"
)
data = load_data()
post_id = f"post_{len(data['posts']) + 1}"
while any(p['id'] == post_id for p in data['posts']):
post_id = f"post_{len(data['posts']) + 1}_{int(time.time())}"
data['posts'].append({
'id': post_id,
'title': title,
'description': description,
'filename': filename,
'type': 'video',
'uploader': uploader,
'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'views': 0,
'likes': [],
'comments': [],
'price': price,
'currency': currency
})
save_data(data)
if os.path.exists(temp_path):
os.remove(temp_path)
return redirect(url_for('profile'))
is_authenticated = 'username' in session
username = session.get('username', None)
user_type = user_data.get('type')
verified = user_data.get('verified', False)
order_count = len(data['orders'].get(username, []))
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Загрузка видео</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 650px;
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 50px;
border-radius: 25px;
box-shadow: var(--shadow);
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.2em;
font-weight: 700;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
#progress-container {
margin-top: 20px;
height: 10px;
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
}
body.dark #progress-container {
background: rgba(255, 255, 255, 0.1);
}
#progress-bar {
width: 0%;
height: 100%;
background: var(--primary);
transition: width 0.4s ease;
}
.flash {
text-align: center;
margin-bottom: 20px;
font-size: 1.1em;
font-weight: 500;
}
.flash.success {
color: #10b981;
}
.flash.error {
color: var(--secondary);
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<h1>Загрузить видео</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form id="upload-form" enctype="multipart/form-data">
<input type="text" name="title" placeholder="Название" required>
<textarea name="description" placeholder="Описание" rows="5"></textarea>
<input type="number" name="price" placeholder="Цена" step="0.01" required>
<select name="currency" required>
<option value="USD">Доллар (USD)</option>
<option value="RUB">Рубль (RUB)</option>
<option value="KGS">Кыргызский сом (KGS)</option>
<option value="KZT">Тенге (KZT)</option>
<option value="UZS">Узбекский сум (UZS)</option>
<option value="UAH">Украинская гривна (UAH)</option>
<option value="PLN">Польский злотый (PLN)</option>
</select>
<input type="file" name="file" accept="video/*" required>
<button type="submit" class="btn">Загрузить</button>
</form>
<div id="progress-container">
<div id="progress-bar"></div>
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
document.getElementById('upload-form').onsubmit = async function(e) {
e.preventDefault();
const formData = new FormData(this);
const progressBar = document.getElementById('progress-bar');
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
progressBar.style.width = percent + '%';
}
};
xhr.onload = function() {
if (xhr.status === 200) {
window.location = '/profile';
} else {
alert('Ошибка загрузки');
}
};
xhr.send(formData);
};
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
};
</script>
</body>
</html>
'''
return render_template_string(html, username=username, is_authenticated=is_authenticated, user_type=user_type, verified=verified, order_count=order_count)
# Заказы продавца
@app.route('/seller_orders', methods=['GET', 'POST'])
def seller_orders():
if 'username' not in session:
flash('Войдите, чтобы просмотреть заказы!', 'error')
return redirect(url_for('login'))
data = load_data()
username = session['username']
user_data = data['users'].get(username, {})
if user_data.get('type') != 'seller' or not user_data.get('verified'):
flash('Только проверенные продавцы могут просматривать заказы!', 'error')
return redirect(url_for('profile'))
orders = data['orders'].get(username, [])
is_authenticated = 'username' in session
user_type = user_data.get('type')
verified = user_data.get('verified', False)
order_count = len(orders)
if request.method == 'POST':
if 'update_status' in request.form:
order_id = request.form.get('order_id')
new_status = request.form.get('status')
for order in orders:
if order['order_id'] == order_id:
order['status'] = new_status
# Обновляем статус в заказах пользователя
for user_order in data['user_orders'].get(order['buyer'], []):
if user_order['order_id'] == order_id:
user_order['status'] = new_status
break
break
save_data(data)
return redirect(url_for('seller_orders'))
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Заказы - {{ username }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 950px;
}
h1 {
font-size: 2.5em;
font-weight: 700;
margin-bottom: 40px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.order-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 30px;
}
.order-item {
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 25px;
border-radius: 20px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .order-item {
background: var(--card-bg-dark);
}
.order-item:hover {
transform: translateY(-15px);
}
.order-item h3 {
font-size: 1.5em;
margin-bottom: 15px;
}
.order-item p {
margin-bottom: 15px;
font-size: 1.1em;
}
.price {
font-weight: 600;
color: var(--primary);
}
.username-link {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.username-link:hover {
text-decoration: underline;
}
select {
width: auto;
display: inline-block;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<h1>Ваши заказы</h1>
<div class="order-grid">
{% if orders %}
{% for order in orders %}
<div class="order-item">
<h3>{{ order['title'] }}</h3>
<p class="price">Цена: {{ order['price'] }} {{ order['currency'] }} x {{ order['quantity'] }} = {{ (order['price']|float * order['quantity'])|round(2) }} {{ order['currency'] }}</p>
<p>Покупатель: <a href="{{ url_for('user_profile', username=order['buyer']) }}" class="username-link">{{ order['buyer'] }}</a></p>
<p>Телефон покупателя: {{ order['buyer_phone'] }}</p>
{% if order['buyer_address'] %}
<p>Адрес доставки: {{ order['buyer_address'] }}</p>
{% endif %}
<p>Дата: {{ order['date'] }}</p>
<form method="POST">
<input type="hidden" name="order_id" value="{{ order['order_id'] }}">
<select name="status" onchange="this.form.submit()">
<option value="pending" {% if order['status'] == 'pending' %}selected{% endif %}>В ожидании</option>
<option value="processing" {% if order['status'] == 'processing' %}selected{% endif %}>В обработке</option>
<option value="completed" {% if order['status'] == 'completed' %}selected{% endif %}>Завершено</option>
</select>
<input type="hidden" name="update_status" value="true">
</form>
</div>
{% endfor %}
{% else %}
<p style="font-size: 1.2em;">У вас пока нет заказов.</p>
{% endif %}
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
};
</script>
</body>
</html>
'''
return render_template_string(html, username=username, orders=orders, is_authenticated=is_authenticated, user_type=user_type, verified=verified, order_count=order_count)
# Мои заказы пользователя
@app.route('/user_orders', methods=['GET'])
def user_orders():
if 'username' not in session:
flash('Войдите, чтобы просмотреть свои заказы!', 'error')
return redirect(url_for('login'))
data = load_data()
username = session['username']
user_data = data['users'].get(username, {})
orders = data['user_orders'].get(username, [])
is_authenticated = 'username' in session
user_type = user_data.get('type')
verified = user_data.get('verified', False)
order_count = len(data['orders'].get(username, [])) if user_type == 'seller' and verified else 0
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Мои заказы - {{ username }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 950px;
}
h1 {
font-size: 2.5em;
font-weight: 700;
margin-bottom: 40px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
.order-grid {
display grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 30px;
}
.order-item {
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 25px;
border-radius: 20px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .order-item {
background: var(--card-bg-dark);
}
.order-item:hover {
transform: translateY(-15px);
}
.order-item h3 {
font-size: 1.5em;
margin-bottom: 15px;
}
.order-item p {
margin-bottom: 15px;
font-size: 1.1em;
}
.price {
font-weight: 600;
color: var(--primary);
}
.username-link {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.username-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<h1>Мои заказы</h1>
<div class="order-grid">
{% if orders %}
{% for order in orders %}
<div class="order-item">
<h3>{{ order['title'] }}</h3>
<p class="price">Цена: {{ order['price'] }} {{ order['currency'] }} x {{ order['quantity'] }} = {{ (order['price']|float * order['quantity'])|round(2) }} {{ order['currency'] }}</p>
<p>Продавец: <a href="{{ url_for('user_profile', username=order['uploader']) }}" class="username-link">{{ order['uploader'] }}</a></p>
<p>Дата: {{ order['date'] }}</p>
<p>Статус: {{ 'В ожидании' if order['status'] == 'pending' else 'В обработке' if order['status'] == 'processing' else 'Завершено' }}</p>
</div>
{% endfor %}
{% else %}
<p style="font-size: 1.2em;">У вас пока нет заказов.</p>
{% endif %}
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
};
</script>
</body>
</html>
'''
return render_template_string(html, username=username, orders=orders, is_authenticated=is_authenticated, user_type=user_type, verified=verified, order_count=order_count)
# Админ-панель
@app.route('/admhosto', methods=['GET', 'POST'])
def admin_panel():
data = load_data()
posts = sorted(data.get('posts', []), key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
pending_orgs = data.get('pending_organizations', [])
is_authenticated = 'username' in session
username = session.get('username', None)
user_type = data['users'].get(username, {}).get('type') if username else None
verified = data['users'].get(username, {}).get('verified', False) if username else False
order_count = len(data['orders'].get(username, [])) if user_type == 'seller' and verified else 0
if request.method == 'POST':
if 'delete' in request.form:
post_id = request.form.get('post_id')
if post_id:
data['posts'] = [p for p in data['posts'] if p['id'] != post_id]
save_data(data)
return redirect(url_for('admin_panel'))
elif 'approve_org' in request.form:
org_username = request.form.get('username')
if org_username:
data['users'][org_username]['verified'] = True
data['pending_organizations'] = [org for org in pending_orgs if org['username'] != org_username]
save_data(data)
return redirect(url_for('admin_panel'))
elif 'reject_org' in request.form:
org_username = request.form.get('username')
if org_username:
data['pending_organizations'] = [org for org in pending_orgs if org['username'] != org_username]
save_data(data)
return redirect(url_for('admin_panel'))
search_query = request.args.get('search', '').strip().lower()
if search_query:
posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()]
html = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
h1, h2 {
font-size: 2.8em;
font-weight: 700;
margin-bottom: 40px;
background: linear-gradient(45deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
color: transparent;
}
h2 {
font-size: 2em;
margin-top: 60px;
}
.search-container {
max-width: 600px;
margin: 0 auto 40px;
position: relative;
}
.search-input {
padding-right: 60px;
background: var(--card-bg-light);
border: none;
box-shadow: var(--shadow);
}
body.dark .search-input {
background: var(--card-bg-dark);
}
.search-btn {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
background: var(--primary);
padding: 10px 20px;
border-radius: 12px;
font-size: 1em;
box-shadow: none;
}
.search-btn:hover {
background: #5439cc;
}
.post-grid, .org-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 30px;
}
.post-item, .org-item {
background: var(--card-bg-light);
backdrop-filter: blur(20px);
padding: 25px;
border-radius: 20px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .post-item, body.dark .org-item {
background: var(--card-bg-dark);
}
.post-item:hover, .org-item:hover {
transform: translateY(-15px);
}
.post-preview {
width: 100%;
height: 240px;
object-fit: cover;
border-radius: 15px;
margin-bottom: 20px;
}
.post-item h3, .org-item h3 {
font-size: 1.5em;
margin-bottom: 15px;
}
.post-item p, .org-item p {
margin-bottom: 15px;
font-size: 1.1em;
}
.delete-btn {
background: var(--secondary);
}
.delete-btn:hover {
background: #e63970;
}
.approve-btn {
background: #10b981;
}
.approve-btn:hover {
background: #0d9f6e;
}
.reject-btn {
background: #ef4444;
}
.reject-btn:hover {
background: #dc2626;
}
.username-link {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.username-link:hover {
text-decoration: underline;
}
.price {
font-weight: 600;
color: var(--primary);
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="cart-float" onclick="toggleCart()" style="display: none;" id="cartFloat">🛒</div>
<div class="cart-modal" id="cartModal">
<h2>Корзина</h2>
<div id="cartItems"></div>
<p class="cart-total" id="cartTotal">Итого: 0</p>
<button class="btn" onclick="checkout()">Оформить заказ</button>
<button class="btn" onclick="toggleCart()">Закрыть</button>
</div>
<div class="container">
<h1>Админ-панель</h1>
<h2>Организации на проверке</h2>
<div class="org-grid">
{% if pending_orgs %}
{% for org in pending_orgs %}
<div class="org-item">
<h3>{{ org['org_name'] }}</h3>
<p>Пользователь: <a href="{{ url_for('user_profile', username=org['username']) }}" class="username-link">{{ org['username'] }}</a></p>
<p>Телефон: {{ org['org_phone'] }}</p>
<p>Адрес: {{ org['org_address'] if org['org_address'] else 'Онлайн' }}</p>
<p>Дата подачи: {{ org['submitted_at'] }}</p>
<form method="POST" style="display: inline;">
<input type="hidden" name="username" value="{{ org['username'] }}">
<button type="submit" name="approve_org" class="btn approve-btn">✔ Подтвердить</button>
<button type="submit" name="reject_org" class="btn reject-btn">✘ Отклонить</button>
</form>
</div>
{% endfor %}
{% else %}
<p style="font-size: 1.2em;">Нет организаций на проверке.</p>
{% endif %}
</div>
<h2>Все видео</h2>
<div class="search-container">
<form method="GET">
<input type="text" name="search" class="search-input" placeholder="Поиск по названию или описанию" value="{{ search_query }}">
<button type="submit" class="search-btn">Искать</button>
</form>
</div>
<div class="post-grid">
{% if posts %}
{% for post in posts %}
<div class="post-item">
<a href="{{ url_for('post_page', post_id=post['id']) }}">
<video class="post-preview" preload="metadata" muted>
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/videos/{{ post['filename'] }}" type="video/mp4">
</video>
<h3>{{ post['title'] }}</h3>
</a>
<p>{{ post['description'] }}</p>
<p class="price">Цена: {{ post['price'] }} {{ post['currency'] }}</p>
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p>
<form method="POST">
<input type="hidden" name="post_id" value="{{ post['id'] }}">
<button type="submit" name="delete" class="btn delete-btn">Удалить</button>
</form>
</div>
{% endfor %}
{% else %}
<p style="font-size: 1.2em;">Видео не найдены.</p>
{% endif %}
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleCart() {
const cartModal = document.getElementById('cartModal');
cartModal.style.display = cartModal.style.display === 'block' ? 'none' : 'block';
renderCart();
}
function renderCart() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
const cartItems = document.getElementById('cartItems');
const cartTotal = document.getElementById('cartTotal');
cartItems.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>Цена: ${item.price} ${item.currency} x ${item.quantity} = ${itemTotal.toFixed(2)} ${item.currency}</p>
<p>Продавец: <a href="/profile/${item.uploader}" class="username-link">${item.uploader}</a></p>
<button class="btn remove-cart-btn" onclick="removeFromCart('${item.postId}')">Удалить</button>
`;
cartItems.appendChild(div);
});
const totalCurrency = cart.length > 0 ? cart[0].currency : '';
cartTotal.textContent = `Итого: ${total.toFixed(2)} ${totalCurrency}`;
}
function removeFromCart(postId) {
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
cart = cart.filter(item => item.postId !== postId);
localStorage.setItem('cart', JSON.stringify(cart));
if (cart.length === 0) document.getElementById('cartFloat').style.display = 'none';
renderCart();
}
function checkout() {
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length === 0) {
alert('Корзина пуста!');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/profile';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'checkout';
input.value = 'true';
const cartData = document.createElement('input');
cartData.type = 'hidden';
cartData.name = 'cart_data';
cartData.value = JSON.stringify(cart);
form.appendChild(input);
form.appendChild(cartData);
document.body.appendChild(form);
form.submit();
localStorage.removeItem('cart');
document.getElementById('cartFloat').style.display = 'none';
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const cart = JSON.parse(localStorage.getItem('cart') || '[]');
if (cart.length > 0) document.getElementById('cartFloat').style.display = 'block';
const videos = document.querySelectorAll('.post-preview');
videos.forEach(video => {
video.addEventListener('loadedmetadata', () => {
video.currentTime = Math.random() * video.duration;
});
});
};
</script>
</body>
</html>
'''
return render_template_string(html, posts=posts, pending_orgs=pending_orgs, is_authenticated=is_authenticated, username=username, repo_id=REPO_ID, search_query=search_query, user_type=user_type, verified=verified, order_count=order_count)
if __name__ == '__main__':
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
app.run(debug=True, host='0.0.0.0', port=7860)