sqlalchemy-connection 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_connection-2.0.1.dist-info/METADATA +26 -0
- sqlalchemy_connection-2.0.1.dist-info/RECORD +33 -0
- sqlalchemy_connection-2.0.1.dist-info/WHEEL +5 -0
- sqlalchemy_connection-2.0.1.dist-info/entry_points.txt +2 -0
- sqlalchemy_connection-2.0.1.dist-info/top_level.txt +1 -0
- sqlalchemy_connector/__init__.py +3 -0
- sqlalchemy_connector/_builder.py +425 -0
- sqlalchemy_connector/cli.py +200 -0
- sqlalchemy_connector/real_generator.py +2908 -0
- sqlalchemy_connector/templates/admin_cart_html_template.html +372 -0
- sqlalchemy_connector/templates/admin_html_template.html +364 -0
- sqlalchemy_connector/templates/admin_users_html_template.html +82 -0
- sqlalchemy_connector/templates/app_template.py +434 -0
- sqlalchemy_connector/templates/base_html_template.html +100 -0
- sqlalchemy_connector/templates/cart_html_template.html +103 -0
- sqlalchemy_connector/templates/catalog_html_template.html +98 -0
- sqlalchemy_connector/templates/checkout_html_template.html +70 -0
- sqlalchemy_connector/templates/dashboard_html_template.html +121 -0
- sqlalchemy_connector/templates/index_html_template.html +91 -0
- sqlalchemy_connector/templates/login_html_template.html +59 -0
- sqlalchemy_connector/templates/models_template.py +65 -0
- sqlalchemy_connector/templates/new_request_html_template.html +49 -0
- sqlalchemy_connector/templates/orders_html_template.html +65 -0
- sqlalchemy_connector/templates/product_form_html_template.html +142 -0
- sqlalchemy_connector/templates/product_html_template.html +131 -0
- sqlalchemy_connector/templates/profile_html_template.html +104 -0
- sqlalchemy_connector/templates/register_html_template.html +183 -0
- sqlalchemy_connector/templates/reviews_html_template.html +104 -0
- sqlalchemy_connector/templates/service_detail_html_template.html +67 -0
- sqlalchemy_connector/templates/service_form_html_template.html +86 -0
- sqlalchemy_connector/templates/services_html_template.html +47 -0
- sqlalchemy_connector/templates/slider_js_template.js +99 -0
- sqlalchemy_connector/templates/style_css_template.css +502 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Управление товарами{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="row g-4">
|
|
7
|
+
<!-- Форма добавления/редактирования -->
|
|
8
|
+
<div class="col-md-5">
|
|
9
|
+
<div class="card border-0 shadow-sm">
|
|
10
|
+
<div class="card-header bg-success text-white">
|
|
11
|
+
<h5 class="mb-0">{{ '✏️ Редактировать товар' if product else '➕ Добавить товар' }}</h5>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="card-body p-4">
|
|
14
|
+
{% if product %}
|
|
15
|
+
<form method="POST" action="{{ url_for('admin_product_edit', product_id=product.id) }}" enctype="multipart/form-data">
|
|
16
|
+
{% else %}
|
|
17
|
+
<form method="POST" action="{{ url_for('admin_product_add') }}" enctype="multipart/form-data">
|
|
18
|
+
{% endif %}
|
|
19
|
+
<div class="mb-3">
|
|
20
|
+
<label class="form-label fw-semibold">Название <span class="text-danger">*</span></label>
|
|
21
|
+
<input type="text" class="form-control" name="name"
|
|
22
|
+
value="{{ product.name if product else '' }}" required>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="mb-3">
|
|
25
|
+
<label class="form-label fw-semibold">Цена (₽) <span class="text-danger">*</span></label>
|
|
26
|
+
<input type="number" class="form-control" name="price" min="0" step="0.01"
|
|
27
|
+
value="{{ product.price if product else '' }}" required>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="mb-3">
|
|
30
|
+
<label class="form-label fw-semibold">Описание</label>
|
|
31
|
+
<textarea class="form-control" name="description" rows="3">{{ product.description if product else '' }}</textarea>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="mb-3">
|
|
34
|
+
<label class="form-label fw-semibold">Количество на складе</label>
|
|
35
|
+
<input type="number" class="form-control" name="stock" min="0"
|
|
36
|
+
value="{{ product.stock if product else 0 }}">
|
|
37
|
+
</div>
|
|
38
|
+
{{CUSTOM_PRODUCT_FIELDS}}
|
|
39
|
+
<div class="mb-3">
|
|
40
|
+
<label class="form-label fw-semibold">Изображение</label>
|
|
41
|
+
{% if product and product.image %}
|
|
42
|
+
<div class="mb-2">
|
|
43
|
+
<img src="{{ url_for('static', filename='uploads/products/' ~ product.image) }}"
|
|
44
|
+
height="80" class="rounded" alt="">
|
|
45
|
+
<small class="text-muted ms-2">Текущее изображение</small>
|
|
46
|
+
</div>
|
|
47
|
+
{% endif %}
|
|
48
|
+
<input type="file" class="form-control" name="image" accept="image/jpeg,image/png,image/webp">
|
|
49
|
+
<small class="text-muted">JPG, PNG, WEBP до 5MB</small>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="d-flex gap-2">
|
|
52
|
+
<button type="submit" class="btn btn-success flex-grow-1">
|
|
53
|
+
{{ '💾 Сохранить' if product else '➕ Добавить' }}
|
|
54
|
+
</button>
|
|
55
|
+
<a href="{{ url_for('admin_products') }}" class="btn btn-outline-secondary">Отмена</a>
|
|
56
|
+
</div>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Список товаров -->
|
|
63
|
+
<div class="col-md-7">
|
|
64
|
+
<div class="card border-0 shadow-sm">
|
|
65
|
+
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
|
66
|
+
<h5 class="mb-0">📦 Все товары</h5>
|
|
67
|
+
<span class="badge bg-light text-dark">{{ products|length }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="card-body p-0">
|
|
70
|
+
{% if products %}
|
|
71
|
+
<div class="table-responsive">
|
|
72
|
+
<table class="table table-hover align-middle mb-0">
|
|
73
|
+
<thead class="table-light">
|
|
74
|
+
<tr>
|
|
75
|
+
<th>Фото</th>
|
|
76
|
+
<th>Название</th>
|
|
77
|
+
<th>Цена</th>
|
|
78
|
+
<th>Склад</th>
|
|
79
|
+
<th>Статус</th>
|
|
80
|
+
<th>Действия</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody>
|
|
84
|
+
{% for p in products %}
|
|
85
|
+
<tr class="{% if not p.is_active %}table-secondary{% endif %}">
|
|
86
|
+
<td>
|
|
87
|
+
{% if p.image %}
|
|
88
|
+
<img src="{{ url_for('static', filename='uploads/products/' ~ p.image) }}"
|
|
89
|
+
width="40" height="40" class="rounded" style="object-fit:cover;" alt="">
|
|
90
|
+
{% else %}
|
|
91
|
+
<div class="bg-light rounded d-flex align-items-center justify-content-center"
|
|
92
|
+
style="width:40px;height:40px;">📷</div>
|
|
93
|
+
{% endif %}
|
|
94
|
+
</td>
|
|
95
|
+
<td>
|
|
96
|
+
<strong>{{ p.name }}</strong>
|
|
97
|
+
{% if p.description %}
|
|
98
|
+
<br><small class="text-muted">{{ p.description[:50] }}{% if p.description|length > 50 %}...{% endif %}</small>
|
|
99
|
+
{% endif %}
|
|
100
|
+
</td>
|
|
101
|
+
<td class="text-nowrap">{{ "%.2f"|format(p.price) }} ₽</td>
|
|
102
|
+
<td>{{ p.stock }}</td>
|
|
103
|
+
<td>
|
|
104
|
+
{% if p.is_active %}
|
|
105
|
+
<span class="badge bg-success">Активен</span>
|
|
106
|
+
{% else %}
|
|
107
|
+
<span class="badge bg-secondary">Снят</span>
|
|
108
|
+
{% endif %}
|
|
109
|
+
</td>
|
|
110
|
+
<td>
|
|
111
|
+
<div class="d-flex gap-1 flex-wrap">
|
|
112
|
+
<a href="{{ url_for('admin_product_edit', product_id=p.id) }}"
|
|
113
|
+
class="btn btn-sm btn-outline-primary" title="Редактировать">✏️</a>
|
|
114
|
+
<form method="POST" action="{{ url_for('admin_product_toggle', product_id=p.id) }}" class="d-inline">
|
|
115
|
+
{% if p.is_active %}
|
|
116
|
+
<button type="submit" class="btn btn-sm btn-outline-warning" title="Снять с продажи">⏸</button>
|
|
117
|
+
{% else %}
|
|
118
|
+
<button type="submit" class="btn btn-sm btn-outline-success" title="Вернуть в продажу">▶️</button>
|
|
119
|
+
{% endif %}
|
|
120
|
+
</form>
|
|
121
|
+
<form method="POST" action="{{ url_for('admin_product_delete', product_id=p.id) }}"
|
|
122
|
+
class="d-inline" onsubmit="return confirm('Удалить товар «{{ p.name }}»?')">
|
|
123
|
+
<button type="submit" class="btn btn-sm btn-outline-danger" title="Удалить">🗑</button>
|
|
124
|
+
</form>
|
|
125
|
+
</div>
|
|
126
|
+
</td>
|
|
127
|
+
</tr>
|
|
128
|
+
{% endfor %}
|
|
129
|
+
</tbody>
|
|
130
|
+
</table>
|
|
131
|
+
</div>
|
|
132
|
+
{% else %}
|
|
133
|
+
<div class="text-center py-5 text-muted">
|
|
134
|
+
<div class="fs-1">📦</div>
|
|
135
|
+
<p>Товаров пока нет. Добавьте первый!</p>
|
|
136
|
+
</div>
|
|
137
|
+
{% endif %}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
{% endblock %}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ product.name }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<nav aria-label="breadcrumb" class="mb-3">
|
|
7
|
+
<ol class="breadcrumb">
|
|
8
|
+
<li class="breadcrumb-item"><a href="{{ url_for('catalog') }}">Каталог</a></li>
|
|
9
|
+
<li class="breadcrumb-item active">{{ product.name }}</li>
|
|
10
|
+
</ol>
|
|
11
|
+
</nav>
|
|
12
|
+
|
|
13
|
+
<div class="row g-4">
|
|
14
|
+
<!-- Изображение -->
|
|
15
|
+
<div class="col-md-5">
|
|
16
|
+
{% if product.image %}
|
|
17
|
+
<img src="{{ url_for('static', filename='uploads/products/' ~ product.image) }}"
|
|
18
|
+
class="img-fluid rounded shadow-sm w-100"
|
|
19
|
+
style="max-height:400px;object-fit:cover;" alt="{{ product.name }}">
|
|
20
|
+
{% else %}
|
|
21
|
+
<div class="bg-light rounded d-flex align-items-center justify-content-center"
|
|
22
|
+
style="height:300px;font-size:80px;">🛍️</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Описание -->
|
|
27
|
+
<div class="col-md-7">
|
|
28
|
+
<h2 class="mb-2">{{ product.name }}</h2>
|
|
29
|
+
|
|
30
|
+
{% if product.description %}
|
|
31
|
+
<p class="text-muted mb-3">{{ product.description }}</p>
|
|
32
|
+
{% endif %}
|
|
33
|
+
|
|
34
|
+
{% if product.stock is defined %}
|
|
35
|
+
<p class="mb-2">
|
|
36
|
+
<span class="badge {% if product.stock > 0 %}bg-success{% else %}bg-danger{% endif %}">
|
|
37
|
+
{% if product.stock > 0 %}В наличии: {{ product.stock }} шт.{% else %}Нет в наличии{% endif %}
|
|
38
|
+
</span>
|
|
39
|
+
</p>
|
|
40
|
+
{% endif %}
|
|
41
|
+
|
|
42
|
+
<div class="fs-2 fw-bold text-success mb-4">{{ product.price }} ₽</div>
|
|
43
|
+
|
|
44
|
+
{% if session.user_id %}
|
|
45
|
+
{% if not product.stock is defined or product.stock > 0 %}
|
|
46
|
+
<form method="POST" action="{{ url_for('cart_add', product_id=product.id) }}"
|
|
47
|
+
class="d-flex gap-2 align-items-center mb-3">
|
|
48
|
+
<input type="number" name="quantity" value="1" min="1"
|
|
49
|
+
max="{{ product.stock or 99 }}"
|
|
50
|
+
class="form-control" style="width:80px;">
|
|
51
|
+
<button type="submit" class="btn btn-primary btn-lg">🛒 В корзину</button>
|
|
52
|
+
</form>
|
|
53
|
+
{% endif %}
|
|
54
|
+
{% else %}
|
|
55
|
+
<a href="{{ url_for('login') }}" class="btn btn-primary btn-lg">Войти для покупки</a>
|
|
56
|
+
{% endif %}
|
|
57
|
+
|
|
58
|
+
{% if session.role == 'admin' %}
|
|
59
|
+
<div class="mt-3 d-flex gap-2">
|
|
60
|
+
<a href="{{ url_for('product_edit', product_id=product.id) }}"
|
|
61
|
+
class="btn btn-outline-secondary btn-sm">✏️ Редактировать</a>
|
|
62
|
+
<form method="POST" action="{{ url_for('product_delete', product_id=product.id) }}"
|
|
63
|
+
onsubmit="return confirm('Удалить товар?')">
|
|
64
|
+
<button type="submit" class="btn btn-outline-danger btn-sm">🗑 Удалить</button>
|
|
65
|
+
</form>
|
|
66
|
+
</div>
|
|
67
|
+
{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Отзывы -->
|
|
72
|
+
{% if reviews is defined %}
|
|
73
|
+
<div class="mt-5">
|
|
74
|
+
<h4 class="mb-3">💬 Отзывы ({{ reviews|length }})</h4>
|
|
75
|
+
|
|
76
|
+
{% if can_review %}
|
|
77
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
78
|
+
<div class="card-header bg-light">
|
|
79
|
+
<h6 class="mb-0">Оставить отзыв</h6>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="card-body">
|
|
82
|
+
<form method="POST" action="{{ url_for('add_review', product_id=product.id) }}">
|
|
83
|
+
<div class="mb-3">
|
|
84
|
+
<label class="form-label">Оценка</label>
|
|
85
|
+
<div class="stars-input d-flex flex-row-reverse justify-content-end">
|
|
86
|
+
{% for i in range(5, 0, -1) %}
|
|
87
|
+
<input type="radio" name="rating" id="star{{ i }}" value="{{ i }}"
|
|
88
|
+
{% if i == 5 %}checked{% endif %}>
|
|
89
|
+
<label for="star{{ i }}">★</label>
|
|
90
|
+
{% endfor %}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="mb-3">
|
|
94
|
+
<label class="form-label">Текст отзыва</label>
|
|
95
|
+
<textarea class="form-control" name="text" rows="3"
|
|
96
|
+
placeholder="Поделитесь впечатлениями..." required></textarea>
|
|
97
|
+
</div>
|
|
98
|
+
<button type="submit" class="btn btn-primary btn-sm">Отправить отзыв</button>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
{% endif %}
|
|
103
|
+
|
|
104
|
+
{% if reviews %}
|
|
105
|
+
{% for review in reviews %}
|
|
106
|
+
<div class="card border-0 shadow-sm mb-3">
|
|
107
|
+
<div class="card-body">
|
|
108
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
109
|
+
<div>
|
|
110
|
+
<strong>{{ review.reviewer.full_name or review.reviewer.email or review.reviewer.login or 'Пользователь' }}</strong>
|
|
111
|
+
<span class="stars ms-2">{{ '★' * review.rating }}{{ '☆' * (5 - review.rating) }}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="d-flex align-items-center gap-2">
|
|
114
|
+
<small class="text-muted">{{ review.created_at.strftime('%d.%m.%Y') }}</small>
|
|
115
|
+
{% if session.role == 'admin' %}
|
|
116
|
+
<form method="POST" action="{{ url_for('admin_delete_review', review_id=review.id) }}">
|
|
117
|
+
<button type="submit" class="btn btn-sm btn-outline-danger py-0 px-1">✕</button>
|
|
118
|
+
</form>
|
|
119
|
+
{% endif %}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<p class="mb-0 mt-2 text-muted">{{ review.text }}</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
{% endfor %}
|
|
126
|
+
{% else %}
|
|
127
|
+
<p class="text-muted">Отзывов пока нет. Будьте первым!</p>
|
|
128
|
+
{% endif %}
|
|
129
|
+
</div>
|
|
130
|
+
{% endif %}
|
|
131
|
+
{% endblock %}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Профиль{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block extra_head %}
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/inputmask@5.0.8/dist/inputmask.min.js"></script>
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
<div class="row g-4">
|
|
11
|
+
<!-- Левая колонка: аватар -->
|
|
12
|
+
<div class="col-md-4">
|
|
13
|
+
<div class="card border-0 shadow-sm text-center">
|
|
14
|
+
<div class="card-body py-4">
|
|
15
|
+
<div class="avatar-wrapper mx-auto mb-3">
|
|
16
|
+
{% if user.avatar %}
|
|
17
|
+
<img src="{{ url_for('static', filename='uploads/avatars/' ~ user.avatar) }}"
|
|
18
|
+
alt="Аватар" class="rounded-circle"
|
|
19
|
+
style="width:100px;height:100px;object-fit:cover;border:3px solid #dee2e6;">
|
|
20
|
+
{% else %}
|
|
21
|
+
<div class="avatar-placeholder rounded-circle mx-auto"
|
|
22
|
+
style="width:100px;height:100px;background:#0d6efd;color:#fff;font-size:40px;display:flex;align-items:center;justify-content:center;">
|
|
23
|
+
{{ user.login[0]|upper }}
|
|
24
|
+
</div>
|
|
25
|
+
{% endif %}
|
|
26
|
+
</div>
|
|
27
|
+
<h5 class="mb-1">{{ user.login }}</h5>
|
|
28
|
+
<span class="badge {% if user.role == 'admin' %}bg-danger{% else %}bg-secondary{% endif %}">
|
|
29
|
+
{{ 'Администратор' if user.role == 'admin' else 'Пользователь' }}
|
|
30
|
+
</span>
|
|
31
|
+
<p class="text-muted small mt-2 mb-0">
|
|
32
|
+
Зарегистрирован: {{ user.created_at.strftime('%d.%m.%Y') }}
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<!-- Форма загрузки аватара -->
|
|
36
|
+
<form method="POST" enctype="multipart/form-data" class="mt-3">
|
|
37
|
+
<input type="hidden" name="action" value="update">
|
|
38
|
+
<label class="btn btn-outline-primary btn-sm w-100" for="avatarInput">
|
|
39
|
+
📷 Изменить фото
|
|
40
|
+
</label>
|
|
41
|
+
<input type="file" id="avatarInput" name="avatar" accept="image/jpeg,image/png"
|
|
42
|
+
class="d-none" onchange="this.form.submit()">
|
|
43
|
+
<small class="text-muted d-block mt-1">JPG, PNG до 2MB</small>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Правая колонка: данные -->
|
|
50
|
+
<div class="col-md-8">
|
|
51
|
+
<!-- Редактирование данных -->
|
|
52
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
53
|
+
<div class="card-header bg-primary text-white">
|
|
54
|
+
<h5 class="mb-0">✏️ Личные данные</h5>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="card-body p-4">
|
|
57
|
+
<form method="POST" enctype="multipart/form-data">
|
|
58
|
+
<input type="hidden" name="action" value="update">
|
|
59
|
+
{{PROFILE_EXTRA_FIELDS}}
|
|
60
|
+
{{CUSTOM_USER_PROFILE_FIELDS}}
|
|
61
|
+
<button type="submit" class="btn btn-primary">💾 Сохранить</button>
|
|
62
|
+
</form>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Смена пароля -->
|
|
67
|
+
<div class="card border-0 shadow-sm">
|
|
68
|
+
<div class="card-header bg-warning text-dark">
|
|
69
|
+
<h5 class="mb-0">🔑 Смена пароля</h5>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="card-body p-4">
|
|
72
|
+
<form method="POST">
|
|
73
|
+
<input type="hidden" name="action" value="change_password">
|
|
74
|
+
<div class="mb-3">
|
|
75
|
+
<label class="form-label fw-semibold">Текущий пароль</label>
|
|
76
|
+
<input type="password" class="form-control" name="old_password"
|
|
77
|
+
placeholder="Введите текущий пароль" required>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="mb-3">
|
|
80
|
+
<label class="form-label fw-semibold">Новый пароль</label>
|
|
81
|
+
<input type="password" class="form-control" name="new_password"
|
|
82
|
+
placeholder="Минимум 8 символов" required>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="mb-3">
|
|
85
|
+
<label class="form-label fw-semibold">Подтверждение нового пароля</label>
|
|
86
|
+
<input type="password" class="form-control" name="confirm_new_password"
|
|
87
|
+
placeholder="Повторите новый пароль" required>
|
|
88
|
+
</div>
|
|
89
|
+
<button type="submit" class="btn btn-warning">🔒 Изменить пароль</button>
|
|
90
|
+
</form>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
{% endblock %}
|
|
96
|
+
|
|
97
|
+
{% block extra_scripts %}
|
|
98
|
+
<script>
|
|
99
|
+
const profilePhone = document.getElementById('profilePhone');
|
|
100
|
+
if (profilePhone) {
|
|
101
|
+
Inputmask('+7 (999) 999-99-99').mask(profilePhone);
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
{% endblock %}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Регистрация{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block extra_head %}
|
|
6
|
+
<!-- Inputmask для маски телефона -->
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/inputmask@5.0.8/dist/inputmask.min.js"></script>
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
{% block content %}
|
|
11
|
+
<div class="row justify-content-center">
|
|
12
|
+
<div class="col-md-7 col-lg-5">
|
|
13
|
+
<div class="card shadow">
|
|
14
|
+
<div class="card-header bg-success text-white">
|
|
15
|
+
<h4 class="mb-0">📝 Регистрация</h4>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="card-body p-4">
|
|
18
|
+
<!-- Общая ошибка -->
|
|
19
|
+
<div id="formError" class="alert alert-danger d-none"></div>
|
|
20
|
+
|
|
21
|
+
<form id="registerForm" novalidate>
|
|
22
|
+
{{REGISTER_LOGIN_FIELD}}
|
|
23
|
+
|
|
24
|
+
<!-- Пароль -->
|
|
25
|
+
<div class="mb-3">
|
|
26
|
+
<label for="password" class="form-label fw-semibold">Пароль <span class="text-danger">*</span></label>
|
|
27
|
+
<div class="input-group">
|
|
28
|
+
<input type="password" class="form-control" id="password" name="password"
|
|
29
|
+
placeholder="Минимум 8 символов" required autocomplete="new-password">
|
|
30
|
+
<button class="btn btn-outline-secondary" type="button" id="togglePw">👁</button>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="invalid-feedback" id="passwordError"></div>
|
|
33
|
+
<!-- Индикатор силы пароля -->
|
|
34
|
+
<div class="mt-1">
|
|
35
|
+
<div class="progress" style="height: 4px;">
|
|
36
|
+
<div class="progress-bar" id="passwordStrength" role="progressbar" style="width: 0%"></div>
|
|
37
|
+
</div>
|
|
38
|
+
<small class="text-muted" id="passwordStrengthText"></small>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Подтверждение пароля -->
|
|
43
|
+
<div class="mb-3">
|
|
44
|
+
<label for="confirm_password" class="form-label fw-semibold">Подтверждение пароля <span class="text-danger">*</span></label>
|
|
45
|
+
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
|
46
|
+
placeholder="Повторите пароль" required autocomplete="new-password">
|
|
47
|
+
<div class="invalid-feedback" id="confirmError"></div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{{REGISTER_EXTRA_FIELDS}}
|
|
51
|
+
{{CUSTOM_USER_REG_FIELDS}}
|
|
52
|
+
|
|
53
|
+
<!-- Согласие -->
|
|
54
|
+
<div class="mb-3 form-check">
|
|
55
|
+
<input type="checkbox" class="form-check-input" id="rules" name="rules" required>
|
|
56
|
+
<label class="form-check-label" for="rules">
|
|
57
|
+
Я согласен(на) на обработку персональных данных <span class="text-danger">*</span>
|
|
58
|
+
</label>
|
|
59
|
+
<div class="invalid-feedback" id="rulesError"></div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<button type="submit" class="btn btn-success w-100 py-2" id="submitBtn">
|
|
63
|
+
Зарегистрироваться
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
66
|
+
<hr>
|
|
67
|
+
<div class="text-center">
|
|
68
|
+
<a href="{{ url_for('login') }}" class="text-decoration-none">
|
|
69
|
+
Уже есть аккаунт? <strong>Войти</strong>
|
|
70
|
+
</a>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{% endblock %}
|
|
77
|
+
|
|
78
|
+
{% block extra_scripts %}
|
|
79
|
+
<script>
|
|
80
|
+
// ─── Маска телефона ───────────────────────────────────────────────
|
|
81
|
+
{{PHONE_MASK_SCRIPT}}
|
|
82
|
+
|
|
83
|
+
// ─── Показать/скрыть пароль ───────────────────────────────────────
|
|
84
|
+
document.getElementById('togglePw').addEventListener('click', function() {
|
|
85
|
+
const pw = document.getElementById('password');
|
|
86
|
+
pw.type = pw.type === 'password' ? 'text' : 'password';
|
|
87
|
+
this.textContent = pw.type === 'password' ? '👁' : '🙈';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── Индикатор силы пароля ────────────────────────────────────────
|
|
91
|
+
document.getElementById('password').addEventListener('input', function() {
|
|
92
|
+
const val = this.value;
|
|
93
|
+
const bar = document.getElementById('passwordStrength');
|
|
94
|
+
const txt = document.getElementById('passwordStrengthText');
|
|
95
|
+
let score = 0;
|
|
96
|
+
if (val.length >= 8) score++;
|
|
97
|
+
if (/[A-Z]/.test(val)) score++;
|
|
98
|
+
if (/[a-z]/.test(val)) score++;
|
|
99
|
+
if (/\d/.test(val)) score++;
|
|
100
|
+
if (/[^A-Za-z0-9]/.test(val)) score++;
|
|
101
|
+
|
|
102
|
+
const levels = [
|
|
103
|
+
{ pct: 0, cls: '', txt: '' },
|
|
104
|
+
{ pct: 20, cls: 'bg-danger', txt: 'Очень слабый' },
|
|
105
|
+
{ pct: 40, cls: 'bg-warning', txt: 'Слабый' },
|
|
106
|
+
{ pct: 60, cls: 'bg-info', txt: 'Средний' },
|
|
107
|
+
{ pct: 80, cls: 'bg-primary', txt: 'Хороший' },
|
|
108
|
+
{ pct: 100, cls: 'bg-success', txt: 'Отличный' },
|
|
109
|
+
];
|
|
110
|
+
const lvl = levels[score];
|
|
111
|
+
bar.style.width = lvl.pct + '%';
|
|
112
|
+
bar.className = 'progress-bar ' + lvl.cls;
|
|
113
|
+
txt.textContent = lvl.txt;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── Проверка совпадения паролей в реальном времени ───────────────
|
|
117
|
+
document.getElementById('confirm_password').addEventListener('input', function() {
|
|
118
|
+
const pw = document.getElementById('password').value;
|
|
119
|
+
if (this.value && this.value !== pw) {
|
|
120
|
+
this.classList.add('is-invalid');
|
|
121
|
+
this.classList.remove('is-valid');
|
|
122
|
+
document.getElementById('confirmError').textContent = 'Пароли не совпадают';
|
|
123
|
+
} else if (this.value) {
|
|
124
|
+
this.classList.remove('is-invalid');
|
|
125
|
+
this.classList.add('is-valid');
|
|
126
|
+
document.getElementById('confirmError').textContent = '';
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
{{REGISTER_LOGIN_CHECK_SCRIPT}}
|
|
131
|
+
|
|
132
|
+
// ─── Отправка формы через AJAX ────────────────────────────────────
|
|
133
|
+
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
|
|
136
|
+
// Сброс ошибок
|
|
137
|
+
document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
|
138
|
+
document.getElementById('formError').classList.add('d-none');
|
|
139
|
+
|
|
140
|
+
const btn = document.getElementById('submitBtn');
|
|
141
|
+
btn.disabled = true;
|
|
142
|
+
btn.textContent = 'Регистрация...';
|
|
143
|
+
|
|
144
|
+
const formData = new FormData(this);
|
|
145
|
+
|
|
146
|
+
fetch('/register', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
body: formData
|
|
149
|
+
})
|
|
150
|
+
.then(r => r.json())
|
|
151
|
+
.then(data => {
|
|
152
|
+
if (data.success) {
|
|
153
|
+
window.location.href = data.redirect;
|
|
154
|
+
} else {
|
|
155
|
+
// Показываем ошибки под полями
|
|
156
|
+
for (const [field, msg] of Object.entries(data.errors)) {
|
|
157
|
+
const input = document.getElementById(field) || document.querySelector('[name="' + field + '"]');
|
|
158
|
+
const errEl = document.getElementById(field + 'Error');
|
|
159
|
+
if (input) {
|
|
160
|
+
input.classList.add('is-invalid');
|
|
161
|
+
}
|
|
162
|
+
if (errEl) {
|
|
163
|
+
errEl.textContent = msg;
|
|
164
|
+
} else {
|
|
165
|
+
// Если нет конкретного поля — показываем общую ошибку
|
|
166
|
+
const fe = document.getElementById('formError');
|
|
167
|
+
fe.textContent = msg;
|
|
168
|
+
fe.classList.remove('d-none');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
btn.disabled = false;
|
|
172
|
+
btn.textContent = 'Зарегистрироваться';
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.catch(() => {
|
|
176
|
+
document.getElementById('formError').textContent = 'Ошибка соединения. Попробуйте ещё раз.';
|
|
177
|
+
document.getElementById('formError').classList.remove('d-none');
|
|
178
|
+
btn.disabled = false;
|
|
179
|
+
btn.textContent = 'Зарегистрироваться';
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
</script>
|
|
183
|
+
{% endblock %}
|