feat: добавление нового API для получения фотографий по VIN - реализована проверка доступа и наличие фотографий - улучшен UX с помощью более информативных ответов - обновлены шаблоны для улучшения дизайна и адаптивности
This commit is contained in:
parent
a62b1b62bd
commit
9673a839c9
98
.cursor/rules/python-flask.mdc
Normal file
98
.cursor/rules/python-flask.mdc
Normal file
@ -0,0 +1,98 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
You are an expert in Python, Flask, and scalable API development.
|
||||
|
||||
Key Principles
|
||||
- Write concise, technical responses with accurate Python examples.
|
||||
- Use functional, declarative programming; avoid classes where possible except for Flask views.
|
||||
- Prefer iteration and modularization over code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., is_active, has_permission).
|
||||
- Use lowercase with underscores for directories and files (e.g., blueprints/user_routes.py).
|
||||
- Favor named exports for routes and utility functions.
|
||||
- Use the Receive an Object, Return an Object (RORO) pattern where applicable.
|
||||
|
||||
Python/Flask
|
||||
- Use def for function definitions.
|
||||
- Use type hints for all function signatures where possible.
|
||||
- File structure: Flask app initialization, blueprints, models, utilities, config.
|
||||
- Avoid unnecessary curly braces in conditional statements.
|
||||
- For single-line statements in conditionals, omit curly braces.
|
||||
- Use concise, one-line syntax for simple conditional statements (e.g., if condition: do_something()).
|
||||
|
||||
Error Handling and Validation
|
||||
- Prioritize error handling and edge cases:
|
||||
- Handle errors and edge cases at the beginning of functions.
|
||||
- Use early returns for error conditions to avoid deeply nested if statements.
|
||||
- Place the happy path last in the function for improved readability.
|
||||
- Avoid unnecessary else statements; use the if-return pattern instead.
|
||||
- Use guard clauses to handle preconditions and invalid states early.
|
||||
- Implement proper error logging and user-friendly error messages.
|
||||
- Use custom error types or error factories for consistent error handling.
|
||||
|
||||
Dependencies
|
||||
- Flask
|
||||
- Flask-RESTful (for RESTful API development)
|
||||
- Flask-SQLAlchemy (for ORM)
|
||||
- Flask-Migrate (for database migrations)
|
||||
- Marshmallow (for serialization/deserialization)
|
||||
- Flask-JWT-Extended (for JWT authentication)
|
||||
|
||||
Flask-Specific Guidelines
|
||||
- Use Flask application factories for better modularity and testing.
|
||||
- Organize routes using Flask Blueprints for better code organization.
|
||||
- Use Flask-RESTful for building RESTful APIs with class-based views.
|
||||
- Implement custom error handlers for different types of exceptions.
|
||||
- Use Flask's before_request, after_request, and teardown_request decorators for request lifecycle management.
|
||||
- Utilize Flask extensions for common functionalities (e.g., Flask-SQLAlchemy, Flask-Migrate).
|
||||
- Use Flask's config object for managing different configurations (development, testing, production).
|
||||
- Implement proper logging using Flask's app.logger.
|
||||
- Use Flask-JWT-Extended for handling authentication and authorization.
|
||||
|
||||
Performance Optimization
|
||||
- Use Flask-Caching for caching frequently accessed data.
|
||||
- Implement database query optimization techniques (e.g., eager loading, indexing).
|
||||
- Use connection pooling for database connections.
|
||||
- Implement proper database session management.
|
||||
- Use background tasks for time-consuming operations (e.g., Celery with Flask).
|
||||
|
||||
Key Conventions
|
||||
1. Use Flask's application context and request context appropriately.
|
||||
2. Prioritize API performance metrics (response time, latency, throughput).
|
||||
3. Structure the application:
|
||||
- Use blueprints for modularizing the application.
|
||||
- Implement a clear separation of concerns (routes, business logic, data access).
|
||||
- Use environment variables for configuration management.
|
||||
|
||||
Database Interaction
|
||||
- Use Flask-SQLAlchemy for ORM operations.
|
||||
- Implement database migrations using Flask-Migrate.
|
||||
- Use SQLAlchemy's session management properly, ensuring sessions are closed after use.
|
||||
|
||||
Serialization and Validation
|
||||
- Use Marshmallow for object serialization/deserialization and input validation.
|
||||
- Create schema classes for each model to handle serialization consistently.
|
||||
|
||||
Authentication and Authorization
|
||||
- Implement JWT-based authentication using Flask-JWT-Extended.
|
||||
- Use decorators for protecting routes that require authentication.
|
||||
|
||||
Testing
|
||||
- Write unit tests using pytest.
|
||||
- Use Flask's test client for integration testing.
|
||||
- Implement test fixtures for database and application setup.
|
||||
|
||||
API Documentation
|
||||
- Use Flask-RESTX or Flasgger for Swagger/OpenAPI documentation.
|
||||
- Ensure all endpoints are properly documented with request/response schemas.
|
||||
|
||||
Deployment
|
||||
- Use Gunicorn or uWSGI as WSGI HTTP Server.
|
||||
- Implement proper logging and monitoring in production.
|
||||
- Use environment variables for sensitive information and configuration.
|
||||
|
||||
Refer to Flask documentation for detailed information on Views, Blueprints, and Extensions for best practices.
|
||||
|
||||
126
app.py
126
app.py
@ -11,6 +11,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import io
|
||||
import json
|
||||
from expiring_dict import ExpiringDict
|
||||
import uuid
|
||||
|
||||
|
||||
|
||||
@ -268,28 +269,17 @@ def search():
|
||||
ua = request.headers.get('User-Agent')
|
||||
app.logger.info(f'AgeNt: {ua}')
|
||||
|
||||
gmedia = False
|
||||
if ua == 'Mediapartners-Google':
|
||||
app.logger.info(f'Find MediaPartner Aget {ua}')
|
||||
if user_ip in gips:
|
||||
gmedia = True
|
||||
else:
|
||||
gmedia = False
|
||||
try:
|
||||
f = open('ips.txt','a')
|
||||
f.write(f'{user_ip}\n')
|
||||
finally:
|
||||
f.close()
|
||||
g_respone = request.form.get('g-recaptcha-response')
|
||||
|
||||
g_respone = request.args.get('g-recaptcha-response')
|
||||
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
|
||||
if capcha_check['success'] == False or capcha_check['score'] <0.5:
|
||||
app.logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
|
||||
app.logger.info(f'Bad google answer: {capcha_check}')
|
||||
if gmedia ==False and app_debug==True:
|
||||
if app_debug==True:
|
||||
req_data = save_request(request)
|
||||
app.logger.info(json.dumps(req_data, indent=4, default=str))
|
||||
return 'bad req', 401
|
||||
return 'google recaptcha req low score', 401
|
||||
|
||||
|
||||
if len(vin) != 17:
|
||||
return 'bad vin!', 500
|
||||
@ -593,7 +583,7 @@ def api_search_v2():
|
||||
return response
|
||||
## ищем в истории
|
||||
cur.execute('''select s.odo,s.odos,s.title,s.dem1,s.dem2,s.year,s.month, json_value(i.jdata, '$.Runs_Drive') RD_Status, json_value(i.jdata, '$.Locate') Sale_Location, json_value(i.jdata, '$.RepCost') as Repear_cost,
|
||||
(select count(1) from salvage_images si where si.vin = s.vin) image_count from salvagedb.salvagedb s left join addinfo i on s.num = i.numid where vin =:p1 and svin = :p2''',
|
||||
(select count(1) from salvage_images si where si.vin = s.vin and fn = 1) image_count from salvagedb.salvagedb s left join addinfo i on s.num = i.numid where vin =:p1 and svin = :p2''',
|
||||
{'p1': vin.upper(), 'p2': vin.upper()[:10]})
|
||||
res = cur.fetchall()
|
||||
|
||||
@ -611,7 +601,7 @@ def api_search_v2():
|
||||
'RD_Status':it[7],
|
||||
'Sale_Location':it[8],
|
||||
'Repear_Cost':it[9],
|
||||
'Image_Count':it[10]
|
||||
'Photo_Count':it[10]
|
||||
})
|
||||
ret = {
|
||||
'status': 'found',
|
||||
@ -650,9 +640,107 @@ def api_search_v2():
|
||||
app.logger.error(traceback.format_exc())
|
||||
return 'bad request!', 500
|
||||
|
||||
@app.route("/api/v2/reqimage")
|
||||
@app.route("/api/v2/reqphoto")
|
||||
def api_reqimage():
|
||||
pass
|
||||
try:
|
||||
access_code = request.args.get('access_code', None)
|
||||
vin = request.args.get('vin', None)
|
||||
|
||||
user_ip = get_ip(request) ## определение ip клиента
|
||||
|
||||
# базовые проверки входящих аргументов
|
||||
if len(vin) != 17 or vin is None:
|
||||
ret = {'status': 'incorrect vin'}
|
||||
response = app.response_class(
|
||||
response=json.dumps(ret),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
if app.debug:
|
||||
app.logger.debug(json.dumps(ret))
|
||||
return response
|
||||
if len(access_code) > 16 or access_code is None:
|
||||
ret = {'status': 'incorrect access_code'}
|
||||
response = app.response_class(
|
||||
response=json.dumps(ret),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
if app.debug:
|
||||
app.logger.debug(json.dumps(ret))
|
||||
return response
|
||||
|
||||
conn = pool.acquire()
|
||||
cur = conn.cursor()
|
||||
|
||||
## проверяем access_code
|
||||
cur.execute(
|
||||
'select t.summ, t.found_price, t.notfound_price, t.decode_price from restfact t where access_code = :p1',
|
||||
{'p1': str(access_code)})
|
||||
res = cur.fetchone()
|
||||
summ = res[0]
|
||||
found_price = res[1]
|
||||
notfound_price = res[2]
|
||||
decode_price = res[3]
|
||||
|
||||
if summ <= 0:
|
||||
ret = {'status': 'You have insufficient balance.'}
|
||||
response = app.response_class(
|
||||
response=json.dumps(ret),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
if app.debug:
|
||||
app.logger.debug(json.dumps(ret))
|
||||
return response
|
||||
|
||||
cur.execute('select count(*) from salvagedb.salvage_images where vin = :p1 and fnd = 1' , {'p1':vin.upper()})
|
||||
res = cur.fetchone()
|
||||
img_count = int(res[0])
|
||||
if img_count<1:
|
||||
ret = {'status': 'Photos not found for this vin.'}
|
||||
response = app.response_class(
|
||||
response=json.dumps(ret),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
if app.debug:
|
||||
app.logger.debug(json.dumps(ret))
|
||||
return response
|
||||
else:
|
||||
#req_id = uuid.uuid4().hex
|
||||
#cur.execute('select rownum,ipath from salvagedb.salvage_images where vin = :p1 and fnd = 1',{'p1':vin.upper()})
|
||||
cur.execute("""select GenImages('{}','{}') uid from dual""".format(access_code, vin.upper()))
|
||||
res = cur.fetchall()
|
||||
req_id = res[0]
|
||||
cur.execute("""select fake_name from salvage_images_req where req_id = '{}}'""".format(req_id))
|
||||
res = cur.fetchall()
|
||||
images = []
|
||||
nm = 0
|
||||
for it in res:
|
||||
images.append({
|
||||
'num':nm,
|
||||
'url':'https://salimages.salvagedb/{}/{}'.format(req_id, it)
|
||||
})
|
||||
nm = nm + 1
|
||||
ret = {'status': 'Image_found', 'Images':images}
|
||||
response = app.response_class(
|
||||
response=json.dumps(ret),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
except:
|
||||
app.logger.error(traceback.format_exc())
|
||||
return 'bad request!', 500
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
5
h origin main
Normal file
5
h origin main
Normal file
@ -0,0 +1,5 @@
|
||||
[33m1799493[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m)[m feat: адаптация шаблонов для мобильных устройств
|
||||
[33mc2d60d9[m[33m ([m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m Развиваем API v2: +search Поправил favicon
|
||||
[33m11f6d56[m Перевод search на post
|
||||
[33m97e85ec[m remove readme.md
|
||||
[33m12f0b91[m Init
|
||||
@ -11,48 +11,35 @@
|
||||
title, but the fact that it has been damaged for sure. Our site helps people avoid buying a damaged vehicle
|
||||
in the past.
|
||||
</p>
|
||||
<div class="wrapper">
|
||||
<!--
|
||||
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<center>
|
||||
|
||||
|
||||
<form id="search_bar" action="/decode" method="post">
|
||||
|
||||
<div style="display: inline-block;">
|
||||
<h2 id="find_your_car">ENTER <a href="https://en.wikipedia.org/wiki/Vehicle_Identification_Number"
|
||||
target="_blank">VIN</a> YOU CAR</h2>
|
||||
<input type="text" id="vininput" name="q" class="search_box ui-autocomplete-input" autocomplete="off"
|
||||
value="" role="textbox" autofocus="" required="" validate="length_between,17,17">
|
||||
<button class="g-recaptcha btn btn-primary" type="submit" data-sitekey="{{capcha_site}}" id="go_button" data-callback='onSubmit' data-action='submit' style="margin-bottom: 10px;">Check It</button>
|
||||
<center>
|
||||
<form id="search_bar" action="/decode" method="post">
|
||||
<div class="search-container">
|
||||
<div class="search-title">
|
||||
<h2 id="find_your_car">ENTER <a href="https://en.wikipedia.org/wiki/Vehicle_Identification_Number" target="_blank">VIN</a> YOU CAR</h2>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="search-wrapper">
|
||||
<input type="text" id="vininput" name="q" class="search_box ui-autocomplete-input"
|
||||
autocomplete="off" value="" role="textbox" autofocus required
|
||||
validate="length_between,17,17">
|
||||
<button class="g-recaptcha btn btn-primary" type="submit"
|
||||
data-sitekey="{{capcha_site}}" id="go_button"
|
||||
data-callback='onSubmit' data-action='submit'>Check It</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</center>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function onSubmit(token) {
|
||||
document.getElementById("search_bar").submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
<script>
|
||||
function onSubmit(token) {
|
||||
document.getElementById("search_bar").submit();
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -10,78 +10,62 @@
|
||||
odometer rollback; and gray market vehicles. We do not claim that the car got in our databank has salvage
|
||||
title, but the fact that it has been damaged for sure. Our site helps people avoid buying a damaged vehicle
|
||||
in the past.
|
||||
</p></div>
|
||||
<div class="wrapper">
|
||||
|
||||
|
||||
<center>
|
||||
<table id="one-column-emphasis">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Make</td>
|
||||
<td>{{det[0][1]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{det[0][2]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Year</td>
|
||||
<td>{{det[0][3]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Body Style</td>
|
||||
<td>{{det[0][4]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Engine</td>
|
||||
<td>{{det[0][5]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cylinders</td>
|
||||
<td>{{det[0][6]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Drive</td>
|
||||
<td>{{det[0][7]}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<table style="width: 986px; height: 78px; text-align: left; margin-left: auto; margin-right: auto;" border="0" cellpadding="2" cellspacing="2">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="1" rowspan="1" style="text-align: center;"><h2>Search salvage history?</h2></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="undefined" valign="undefined">
|
||||
<div style="text-align: center;">
|
||||
<form name="search_bar" id="search_bar" action="/search" method="post">
|
||||
<input id="make_model" class="search_box ui-autocomplete-input" name="q" size="55" value="{{vin}}" autocomplete="off" spellcheck="false" type="text">
|
||||
<button class="g-recaptcha btn btn-primary" data-sitekey="{{capcha_site}}" id="go_button" data-callback='onSubmit' data-action='submit' style="margin-bottom: 10px;">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</center>
|
||||
<br>
|
||||
<center>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="details-container">
|
||||
<div class="car-details">
|
||||
<table id="one-column-emphasis">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Make</td>
|
||||
<td>{{det[0][1]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{det[0][2]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Year</td>
|
||||
<td>{{det[0][3]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Body Style</td>
|
||||
<td>{{det[0][4]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Engine</td>
|
||||
<td>{{det[0][5]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cylinders</td>
|
||||
<td>{{det[0][6]}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Drive</td>
|
||||
<td>{{det[0][7]}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<h2>Search salvage history?</h2>
|
||||
<form name="search_bar" id="search_bar" action="/search" method="post">
|
||||
<div class="search-wrapper">
|
||||
<input id="make_model" class="search_box ui-autocomplete-input" name="q"
|
||||
value="{{vin}}" autocomplete="off" spellcheck="false" type="text">
|
||||
<button class="g-recaptcha btn btn-primary" data-sitekey="{{capcha_site}}"
|
||||
id="go_button" data-callback='onSubmit' data-action='submit'>Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<script>
|
||||
function onSubmit(token) {
|
||||
document.getElementById("search_bar").submit();
|
||||
document.getElementById("search_bar").submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<br><br>
|
||||
{% endblock %}
|
||||
<input value="Search" class="btn btn-primary" type="submit" id="go_button">
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -278,52 +278,137 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.navbar-inner {
|
||||
padding: 0 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.navbar .brand {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar .brand img {
|
||||
height: 28px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.navbar .nav {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar .nav > li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar .nav > li > a {
|
||||
display: block;
|
||||
padding: 15px 20px;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.navbar .nav > li > a:hover {
|
||||
color: #fff;
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.navbar .nav > li.active > a {
|
||||
color: #fff;
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.navbar .btn-navbar {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar .btn-navbar .icon-bar {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
background-color: #333;
|
||||
margin: 4px 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-inner {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.navbar .nav-collapse {
|
||||
clear: both;
|
||||
padding: 0;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.navbar .nav {
|
||||
float: none;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
flex-direction: column;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.navbar .nav > li {
|
||||
float: none;
|
||||
display: block;
|
||||
.navbar .nav.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.navbar .nav > li > a {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar .nav > li > a:hover {
|
||||
color: #fff;
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.navbar .nav > li.active > a {
|
||||
color: #fff;
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.navbar .btn-navbar {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
background: #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.navbar .nav-collapse {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
}
|
||||
|
||||
.navbar .nav-collapse.in {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.navbar .brand {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.navbar .brand img {
|
||||
height: 24px;
|
||||
.navbar .btn-navbar .icon-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -351,11 +436,20 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btnNavbar = document.querySelector('.btn-navbar');
|
||||
var navCollapse = document.querySelector('.nav-collapse');
|
||||
var nav = document.querySelector('.navbar .nav');
|
||||
|
||||
if (btnNavbar && navCollapse) {
|
||||
if (btnNavbar && nav) {
|
||||
btnNavbar.addEventListener('click', function() {
|
||||
navCollapse.classList.toggle('in');
|
||||
this.classList.toggle('active');
|
||||
nav.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Закрытие меню при клике вне его
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!nav.contains(e.target) && !btnNavbar.contains(e.target)) {
|
||||
btnNavbar.classList.remove('active');
|
||||
nav.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user