|
|
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) |