- Created comprehensive badges-telegram-style.css (240 lines) based on Telegram Web K - Updated all badge markup in templates (navbar, sidebar, chat_list, chat_detail, chat_title) - Replaced Bootstrap badge classes with .badge-telegram classes - Implemented Telegram specifications: 20px height, 10px radius, 11px font, 600 weight - Added smooth animations: pulse (on update), animate-in (on mount), animate-out (on unmount) - Updated JavaScript: replaced .d-none with style.display for badge visibility - Updated 5 JS files: bottom-navbar, notification-manager, chatBadgeHandler, chatBadgeManager, chatListRealtime - Added pulse animation on badge value increase - Improved shadows and transitions with cubic-bezier easing - Added badges-telegram-style.css to base.html for global availability - Created documentation: TELEGRAM_BADGES_GUIDE.md, TELEGRAM_BADGES_UPDATE.md Technical details: - Research: Analyzed 50+ files from Telegram Web K GitHub repository - Perfect circles: border-radius: 10px (not 50%) - Spring animation: cubic-bezier(0.175, 0.885, 0.32, 1.275) - Auto-hide: :empty and [data-count='0'] selectors - Responsive design + dark theme support
180 lines
8.0 KiB
HTML
180 lines
8.0 KiB
HTML
{% load static api_tags %}
|
||
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="color-scheme" content="light dark">
|
||
{% if user.is_authenticated %}
|
||
<meta name="api-access" content="{% api_access_token request %}">
|
||
<meta name="user-id" content="{{ user.id }}">
|
||
{% endif %}
|
||
|
||
{# КРИТИЧНО: Применяем тему ДО загрузки CSS для предотвращения FOUC #}
|
||
<script src="{% static 'js/utils/themeInitializer.js' %}"></script>
|
||
|
||
<title>{% block title %}HiRo{% endblock %}</title>
|
||
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'img/favicon-32.png' %}">
|
||
<link rel="apple-touch-icon" href="{% static 'img/apple-touch-icon.png' %}">
|
||
<link rel="icon" type="image/png" sizes="192x192" href="{% static 'img/logo-192.png' %}">
|
||
|
||
{# Preload критичных CSS для ускорения загрузки #}
|
||
<link rel="preload" href="{% static 'css/bootstrap-custom.css' %}" as="style">
|
||
<link rel="preload" href="{% static 'css/variables.css' %}" as="style">
|
||
<link rel="preload" href="{% static 'css/components/common.css' %}" as="style">
|
||
|
||
<!-- Bootstrap / Icons / FullCalendar -->
|
||
<!-- Bootstrap CSS (локальная версия) -->
|
||
<!-- Кастомный Bootstrap с нашей системой отступов -->
|
||
<link rel="stylesheet" href="{% static 'css/bootstrap-custom.css' %}">
|
||
<link href="{% static 'vendor/bootstrap-icons/bootstrap-icons.min.css' %}" rel="stylesheet">
|
||
|
||
<!-- CSS Components -->
|
||
<link rel="stylesheet" href="{% static 'css/variables.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/common.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/badges-telegram-style.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/spacing-utils.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/layout-spacing.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/buttons.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/ios-search.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/base-app.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/ios-components.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/card-list.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/notifications/notifications.css' %}">
|
||
|
||
{% block extra_css %}{% endblock %}
|
||
</head>
|
||
<body class="min-vh-100">
|
||
{# Устанавливаем глобальные переменные в самом начале #}
|
||
{% if user.is_authenticated %}
|
||
<script>
|
||
window.currentUserId = {{ user.id }};
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% include "includes/navbar.html" %}
|
||
|
||
<div class="container-xxl">
|
||
<div class="app-layout {% if user.is_authenticated %}with-sidebar with-rightbar{% endif %}">
|
||
{% if user.is_authenticated %}
|
||
{% include "includes/sidebar.html" %}
|
||
{% endif %}
|
||
|
||
<main class="main">
|
||
{% block content %}{% endblock %}
|
||
</main>
|
||
|
||
{% if user.is_authenticated %}
|
||
{% include "includes/rightbar_calendar.html" %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JS -->
|
||
<!-- Bootstrap JS (локальная версия) -->
|
||
<script src="{% static 'vendor/bootstrap/bootstrap.bundle.min.js' %}"></script>
|
||
|
||
<!-- FullCalendar (локальные версии) -->
|
||
<script src="{% static 'vendor/fullcalendar/fullcalendar.min.js' %}" defer></script>
|
||
<script src="{% static 'vendor/fullcalendar/locales-all.min.js' %}" defer></script>
|
||
|
||
{# Универсальный модуль для автоматического расширения textarea #}
|
||
<script src="{% static 'js/components/textareaAutoExpand.js' %}" defer></script>
|
||
|
||
{# Импорт утилит из модулей #}
|
||
<script type="module">
|
||
import { esc, norm, escAttr, truncate, getInitials } from '{% static "js/utils/stringUtils.js" %}';
|
||
import { debounce, throttle, delay } from '{% static "js/utils/timing.js" %}';
|
||
import { smoothScrollTo, scrollToTop, isElementInViewport } from '{% static "js/utils/scroll.js" %}';
|
||
import { initNavbarHeight } from '{% static "js/components/navbarHeight.js" %}';
|
||
import { initLikeHandler } from '{% static "js/components/likeHandler.js" %}';
|
||
|
||
// Экспортируем в window для обратной совместимости с существующим кодом
|
||
window.esc = esc;
|
||
window.norm = norm;
|
||
window.escAttr = escAttr;
|
||
window.truncate = truncate;
|
||
window.getInitials = getInitials;
|
||
window.debounce = debounce;
|
||
window.throttle = throttle;
|
||
window.delay = delay;
|
||
window.smoothScrollTo = smoothScrollTo;
|
||
window.scrollToTop = scrollToTop;
|
||
window.isElementInViewport = isElementInViewport;
|
||
|
||
// Инициализация компонентов
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const navbarHeight = initNavbarHeight();
|
||
const likeHandler = initLikeHandler();
|
||
|
||
if (navbarHeight) window.navbarHeight = navbarHeight;
|
||
if (likeHandler) window.likeHandler = likeHandler;
|
||
});
|
||
</script>
|
||
|
||
{# ========== UNIVERSAL USER WEBSOCKET ========== #}
|
||
{# Универсальное WebSocket-подключение для всех real-time обновлений #}
|
||
{% if user.is_authenticated %}
|
||
<script type="module">
|
||
import { initUserWebSocket } from "{% static 'js/components/userWebSocket.js' %}";
|
||
|
||
// Базовая инициализация WebSocket (без chat-специфичной логики)
|
||
const userWs = initUserWebSocket({
|
||
userId: {{ user.id }}
|
||
});
|
||
|
||
// Экспортируем в window для доступа из других модулей
|
||
if (userWs) {
|
||
window.userWebSocket = userWs;
|
||
window.dispatchEvent(new CustomEvent('user:ws-ready', {
|
||
detail: { api: userWs, ws: userWs.ws }
|
||
}));
|
||
}
|
||
</script>
|
||
|
||
<!-- Notification Manager -->
|
||
<script type="module" src="{% static 'js/notifications/notification-manager.js' %}"></script>
|
||
|
||
<!-- Web Push Notifications -->
|
||
<script type="module">
|
||
import { pushNotifications } from "{% static 'js/notifications/push-notifications.js' %}";
|
||
|
||
// Инициализация Web Push после загрузки страницы
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
const initialized = await pushNotifications.init();
|
||
|
||
if (initialized) {
|
||
console.log('[WebPush] Инициализирован, подписка:', pushNotifications.isSubscribed);
|
||
|
||
// Если разрешение еще не спрошено - запрашиваем его
|
||
if (pushNotifications.getPermissionStatus() === 'default' && !pushNotifications.isSubscribed) {
|
||
console.log('[WebPush] Запрашиваем разрешение на уведомления...');
|
||
|
||
// Небольшая задержка, чтобы пользователь успел сориентироваться
|
||
setTimeout(async () => {
|
||
const permission = await pushNotifications.requestPermission();
|
||
if (permission === 'granted') {
|
||
console.log('[WebPush] Разрешение получено, подписываемся...');
|
||
await pushNotifications.subscribe();
|
||
} else {
|
||
console.log('[WebPush] Разрешение не получено');
|
||
}
|
||
}, 2000);
|
||
}
|
||
|
||
// Если разрешение дано, но нет подписки - подписываем автоматически
|
||
if (pushNotifications.getPermissionStatus() === 'granted' && !pushNotifications.isSubscribed) {
|
||
console.log('[WebPush] Разрешение есть, подписываемся...');
|
||
await pushNotifications.subscribe();
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% block extra_js %}{% endblock %}
|
||
</body>
|
||
</html>
|