Игорь Надеин ddf22c47da fix: автоматическое обновление JWT токенов
- ApiClient теперь обрабатывает 401 и 403 ошибки для обновления токена
- Добавлен JWTRefreshMiddleware для проактивного обновления токенов за 5 минут до истечения
- Добавлена настройка JWT_REFRESH_THRESHOLD_MINUTES (по умолчанию 5)
- Исправлена проблема с периодическими 403 ошибками после истечения access token
2025-11-20 18:52:29 +03:00

136 lines
6.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% load static %}
<link rel="stylesheet" href="{% static 'css/components/navbar.css' %}">
<script src="{% static 'js/navbar-mobile-search.js' %}" defer></script>
<nav class="navbar navbar-expand bg-primary app-navbar" data-bs-theme="dark">
<div class="container-fluid align-items-center">
{% if user.is_authenticated %}
<!-- Моб. меню (иконка без подложки) -->
<button class="menu-toggle d-lg-none me-2 navbar-icon" type="button"
data-bs-toggle="offcanvas" data-bs-target="#sidebar" aria-controls="sidebar"
aria-label="Открыть меню">
<i class="bi-list fs-4"></i>
</button>
{% endif %}
{% load static %}
<a class="navbar-brand d-flex align-items-center gap-2 me-3 navbar-brand-item" href="/">
<img src="{% static BRAND_LOGO %}"
alt="{{ BRAND_NAME }}"
width="28" height="28"
class="align-text-bottom"
style="image-rendering:auto">
<span class="fw-semibold d-none d-sm-inline">{{ BRAND_NAME }}</span>
</a>
{% if user.is_authenticated %}
<!-- Группа: поиск + колокол -->
<div class="d-flex align-items-center ms-4 gap-3">
<!-- Поиск: иконка на мобильном, поле на десктопе -->
<form class="d-flex align-items-center mobile-search-form" role="search"
action="{% url 'search:search' %}" method="get" autocomplete="off">
<!-- Мобильная версия: иконка или поле -->
<button type="button" class="btn btn-link text-white p-0 d-md-none mobile-search-toggle" aria-label="Поиск">
<i class="bi-search fs-5"></i>
</button>
<div class="mobile-search-input d-none align-items-center gap-2">
<input
class="form-control form-control-sm rounded-pill bg-white bg-opacity-10 border-0 text-white shadow-sm"
type="search" name="q" placeholder="Поиск…" aria-label="Поиск" id="mobileSearchInput"
style="width: 200px; height: 36px; padding-left: 12px;">
<button type="submit" class="btn btn-link text-white p-0" aria-label="Найти">
<i class="bi-arrow-right-circle fs-5"></i>
</button>
</div>
<!-- Десктопная версия: поле -->
<div class="position-relative ios-search d-none d-md-block" style="max-width: clamp(160px, 28vw, 300px);">
<i class="bi-search"></i>
<input
class="form-control form-control-sm rounded-pill ios-search-input bg-white bg-opacity-10 border-0 text-white shadow-sm"
type="search" name="q" placeholder="Поиск…" aria-label="Поиск" id="desktopSearchInput"
style="width: 100%; height: 36px;">
</div>
</form>
<!-- Уведомления: только иконка + плотный бейдж -->
<div class="position-relative navbar-icon" id="notificationBell">
<button class="btn btn-link text-white p-0"
id="notificationToggle"
type="button"
title="Уведомления"
aria-label="Уведомления">
<span class="bell-wrapper">
<i class="bi-bell fs-5"></i>
<span class="bell-dot d-none" id="notificationBadge">0</span>
</span>
</button>
</div>
</div>
{% endif %}
<div class="ms-auto d-flex align-items-center navbar-right">
{% if user.is_authenticated %}
<div class="dropdown navbar-icon">
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle no-caret"
id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="min-width:40px;">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="Аватар {{ user.get_short_name }}" width="36" height="36"
class="rounded-circle shadow-sm object-fit-cover">
{% else %}
<span class="bg-secondary text-white rounded-circle d-inline-flex align-items-center justify-content-center"
style="width:36px;height:36px;font-size:1.2rem;">
{{ user.get_short_name|slice:":1"|upper }}
</span>
{% endif %}
</a>
<ul class="dropdown-menu dropdown-menu-end rounded-3 shadow-sm" aria-labelledby="userDropdown">
<li class="px-3 py-2 text-muted small">
{{ user.get_short_name }}<br>
<span class="text-secondary">{{ user.email }}</span>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'employees:profile' %}">
<i class="bi-person me-2"></i>Профиль</a></li>
<li>
<a class="dropdown-item" href="{% url 'auth_front:logout' %}">
<i class="bi-box-arrow-right me-2"></i>Выйти
</a>
</li>
</ul>
</div>
{% else %}
<a class="btn btn-outline-light rounded-pill me-2" href="{% url 'auth_front:login' %}">Войти</a>
<a class="btn btn-light rounded-pill" href="{% url 'auth_front:register' %}">Регистрация</a>
{% endif %}
</div>
</div>
</nav>
<!-- Dropdown меню уведомлений (вне navbar для правильного z-index) -->
{% if user.is_authenticated %}
<div class="notification-dropdown-menu" id="notificationDropdown" style="display: none;">
<div class="notification-dropdown-header">
<span class="fw-semibold">Уведомления</span>
<a href="{% url 'notifications:list' %}" class="btn btn-sm btn-link">Все</a>
</div>
<div class="notification-dropdown-body" id="notificationList">
<div class="text-center py-3 text-muted">
<i class="bi-bell-slash"></i>
<div class="small">Нет новых уведомлений</div>
</div>
</div>
<div class="notification-dropdown-divider"></div>
<div class="px-3 py-2">
<a href="{% url 'notifications:settings' %}" class="btn btn-sm btn-outline-primary w-100">
<i class="bi-gear me-1"></i>Настройки уведомлений
</a>
</div>
</div>
{% endif %}