/
Rodin_xx12
/
hawk
Обзор
Документация
Войти
/
Rodin_xx12
/
hawk
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
patch
DevProject.txt
1 929 строк
72 KB
Rodin_xx12
upload files
23 фев 2026, 18:26
Верифицирован
23 фев 2026, 18:26
673b0cd
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes"> <title>Локальный чат — красиво и с медиа</title> <!-- Font Awesome 6 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <!-- Google Fonts Inter --> <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet"> <style> /* CSS переменные для единой цветовой схемы */ :root { --bg-primary: #0e1621; --bg-secondary: #17212b; --bg-tertiary: #1e2a36; --bg-input: #242f3d; --accent: #2b5278; --accent-hover: #3e6b96; --accent-light: #6ab2f2; --text-primary: #eaeef2; --text-secondary: #8b9eb5; --text-muted: #708499; --danger: #b71c1c; --danger-hover: #d32f2f; --border-color: #2b3947; --shadow: 0 8px 20px rgba(0, 0, 0, 0.4); --transition: all 0.2s ease; } * { margin: 0; padding: 0; box-sizing: border-box; } body { background: var(--bg-primary); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 10px; color: var(--text-primary); line-height: 1.5; } .app { width: 100%; max-width: 1200px; height: 95vh; background: var(--bg-secondary); border-radius: 24px; box-shadow: var(--shadow); display: flex; flex-direction: column; overflow: hidden; } /* Шапка */ .header { background: var(--bg-tertiary); color: var(--text-primary); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); } .header h2 { font-weight: 600; font-size: 1.4rem; letter-spacing: -0.3px; background: linear-gradient(135deg, var(--accent-light), #8ab4f8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .user-info { display: flex; align-items: center; gap: 16px; } .user-avatar { width: 42px; height: 42px; border-radius: 50%; background: var(--accent); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 1.2rem; cursor: pointer; position: relative; overflow: hidden; transition: var(--transition); box-shadow: 0 2px 8px rgba(0,0,0,0.3); } .user-avatar:hover { transform: scale(1.05); filter: brightness(1.1); } .user-avatar img { width: 100%; height: 100%; object-fit: cover; } .user-avatar i { font-size: 1.3rem; color: white; } .user-name { font-weight: 500; font-size: 1rem; } .logout-btn { background: transparent; border: 1px solid var(--border-color); color: var(--text-secondary); padding: 8px 16px; border-radius: 40px; cursor: pointer; font-size: 0.9rem; font-weight: 500; transition: var(--transition); display: flex; align-items: center; gap: 8px; } .logout-btn:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); border-color: var(--accent-light); } /* Экран входа */ .login-screen { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 24px; background: var(--bg-secondary); } .login-screen h3 { margin-bottom: 28px; font-size: 2rem; font-weight: 600; color: var(--accent-light); text-shadow: 0 2px 5px rgba(0,0,0,0.3); } .login-input { width: 100%; max-width: 340px; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 50px; font-size: 1rem; outline: none; background: var(--bg-input); color: var(--text-primary); transition: var(--transition); margin-bottom: 16px; } .login-input:focus { border-color: var(--accent-light); box-shadow: 0 0 0 3px rgba(106, 178, 242, 0.2); } .login-btn { width: 100%; max-width: 340px; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 50px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: var(--transition); margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; } .login-btn:hover { background: var(--accent-hover); transform: translateY(-2px); box-shadow: 0 5px 15px rgba(43, 82, 120, 0.4); } .admin-btn { background: var(--danger); } .admin-btn:hover { background: var(--danger-hover); } /* Модальное окно (админ пароль) */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; z-index: 2000; transition: opacity 0.3s; } .modal.hidden { display: none; } .modal-content { background: var(--bg-secondary); padding: 32px; border-radius: 32px; width: 360px; max-width: 90%; box-shadow: var(--shadow); border: 1px solid var(--border-color); animation: modalSlide 0.3s ease; } @keyframes modalSlide { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .modal-content h3 { margin-bottom: 24px; font-size: 1.6rem; font-weight: 600; color: var(--accent-light); text-align: center; } .modal-content input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color); border-radius: 40px; background: var(--bg-input); color: var(--text-primary); font-size: 1rem; margin-bottom: 20px; outline: none; transition: var(--transition); } .modal-content input:focus { border-color: var(--accent-light); } .modal-content button { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 40px; font-weight: 600; cursor: pointer; transition: var(--transition); margin-bottom: 10px; display: flex; align-items: center; justify-content: center; gap: 8px; } .modal-content button:last-child { background: transparent; border: 1px solid var(--border-color); color: var(--text-secondary); } .modal-content button:last-child:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); } /* Основное приложение */ .main { flex: 1; display: flex; overflow: hidden; background: var(--bg-primary); } /* Сайдбар */ .sidebar { width: 300px; background: var(--bg-secondary); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; } .sidebar.collapsed { width: 0; } .sidebar-header { padding: 20px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); } .sidebar-header span { font-weight: 600; font-size: 1.2rem; color: var(--accent-light); } .new-chat-btn { background: var(--accent); color: white; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 1rem; display: flex; align-items: center; justify-content: center; transition: var(--transition); } .new-chat-btn:hover { background: var(--accent-hover); transform: rotate(90deg); } /* Поле поиска */ .search-box { padding: 12px 16px; position: relative; border-bottom: 1px solid var(--border-color); } .search-box i { position: absolute; left: 28px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 0.9rem; pointer-events: none; } .search-box input { width: 100%; padding: 12px 16px 12px 42px; border: 1px solid var(--border-color); border-radius: 40px; background: var(--bg-input); color: var(--text-primary); font-size: 0.95rem; outline: none; transition: var(--transition); } .search-box input:focus { border-color: var(--accent-light); background: var(--bg-tertiary); } .search-box input::placeholder { color: var(--text-muted); } .chat-list { flex: 1; overflow-y: auto; padding: 12px 8px; scrollbar-width: thin; scrollbar-color: var(--accent) var(--bg-tertiary); } .chat-list::-webkit-scrollbar { width: 5px; } .chat-list::-webkit-scrollbar-track { background: var(--bg-tertiary); } .chat-list::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 10px; } .chat-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 4px; transition: var(--transition); animation: fadeIn 0.3s; } @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } .chat-item:hover { background: var(--bg-tertiary); } .chat-item.active { background: var(--accent); } .chat-avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--accent-light); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 1.2rem; color: white; flex-shrink: 0; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } .chat-avatar img { width: 100%; height: 100%; object-fit: cover; } .chat-info { flex: 1; overflow: hidden; } .chat-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; } .chat-meta { font-size: 0.8rem; color: var(--text-secondary); display: flex; gap: 8px; align-items: center; } .chat-meta i { font-size: 0.7rem; color: var(--text-muted); } .chat-actions { display: flex; gap: 8px; opacity: 0; transition: opacity 0.2s; } .chat-item:hover .chat-actions { opacity: 1; } .chat-action-btn { background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1rem; padding: 4px; border-radius: 8px; transition: var(--transition); } .chat-action-btn:hover { background: rgba(255,255,255,0.1); color: var(--text-primary); } /* Основная область чата */ .chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .chat-header { background: var(--bg-tertiary); padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); } .chat-header-left { display: flex; align-items: center; gap: 12px; } .toggle-sidebar { background: transparent; border: none; color: var(--text-secondary); font-size: 1.5rem; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: var(--transition); } .toggle-sidebar:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); } .chat-title { display: flex; align-items: center; gap: 12px; } .chat-title-avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--accent-light); display: flex; align-items: center; justify-content: center; overflow: hidden; cursor: pointer; transition: var(--transition); box-shadow: 0 2px 8px rgba(0,0,0,0.2); } .chat-title-avatar:hover { transform: scale(1.05); filter: brightness(1.1); } .chat-title-avatar img { width: 100%; height: 100%; object-fit: cover; } .chat-title-avatar span { font-size: 1.5rem; color: white; } .chat-title-text { font-weight: 600; font-size: 1.2rem; } .chat-members-count { font-size: 0.8rem; color: var(--text-secondary); display: flex; align-items: center; gap: 4px; margin-top: 4px; } .add-member-btn { background: var(--accent); border: none; color: white; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; font-size: 1rem; display: flex; align-items: center; justify-content: center; transition: var(--transition); } .add-member-btn:hover { background: var(--accent-hover); transform: rotate(90deg); } /* Сообщения */ .messages-area { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; scrollbar-width: thin; scrollbar-color: var(--accent) var(--bg-tertiary); } .messages-area::-webkit-scrollbar { width: 5px; } .messages-area::-webkit-scrollbar-track { background: var(--bg-tertiary); } .messages-area::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 10px; } .message-row { display: flex; align-items: flex-end; gap: 8px; max-width: 100%; animation: messageAppear 0.2s ease; } @keyframes messageAppear { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .message-row.own { justify-content: flex-end; } .message-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--accent-light); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; flex-shrink: 0; overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .message-avatar img { width: 100%; height: 100%; object-fit: cover; } .message-bubble { background: var(--bg-tertiary); padding: 12px 16px; border-radius: 22px 22px 22px 4px; max-width: 70%; word-wrap: break-word; color: var(--text-primary); position: relative; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } .message-bubble.own { background: var(--accent); border-radius: 22px 22px 4px 22px; } .message-header { display: flex; justify-content: space-between; align-items: center; font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 6px; } .message-author { font-weight: 700; color: var(--accent-light); } .own .message-author { color: white; } .message-time { color: var(--text-secondary); font-size: 0.7rem; display: flex; align-items: center; gap: 4px; } .message-media { max-width: 100%; max-height: 200px; border-radius: 12px; margin-top: 8px; cursor: pointer; border: 2px solid transparent; transition: var(--transition); } .message-media:hover { border-color: var(--accent-light); } .message-video { max-width: 100%; max-height: 300px; border-radius: 12px; margin-top: 8px; border: 2px solid transparent; } .message-actions { display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end; opacity: 0; transition: opacity 0.2s; } .message-row:hover .message-actions { opacity: 1; } .message-action { cursor: pointer; font-size: 0.9rem; padding: 4px; border-radius: 4px; color: var(--text-secondary); transition: var(--transition); } .message-action:hover { color: var(--text-primary); background: rgba(255,255,255,0.1); } .message-text { font-size: 0.95rem; line-height: 1.5; white-space: pre-wrap; } /* Область ввода */ .input-area { background: var(--bg-secondary); padding: 16px 20px; display: flex; gap: 12px; align-items: flex-end; border-top: 1px solid var(--border-color); flex-wrap: wrap; position: relative; } .attach-buttons { display: flex; gap: 8px; } .attach-btn { background: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-secondary); width: 52px; height: 52px; border-radius: 50%; cursor: pointer; font-size: 1.3rem; display: flex; align-items: center; justify-content: center; transition: var(--transition); } .attach-btn:hover { background: var(--accent); color: white; border-color: var(--accent); transform: scale(1.05); } .emoji-btn { background: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-secondary); width: 52px; height: 52px; border-radius: 50%; cursor: pointer; font-size: 1.3rem; display: flex; align-items: center; justify-content: center; transition: var(--transition); } .emoji-btn:hover { background: var(--accent); color: white; border-color: var(--accent); transform: scale(1.05); } .emoji-panel { position: absolute; bottom: 100%; left: 20px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 16px; padding: 12px; display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; max-width: 400px; max-height: 200px; overflow-y: auto; box-shadow: var(--shadow); z-index: 100; margin-bottom: 8px; } .emoji-panel.hidden { display: none; } .emoji-item { font-size: 1.5rem; cursor: pointer; text-align: center; padding: 4px; border-radius: 8px; transition: var(--transition); } .emoji-item:hover { background: var(--accent); transform: scale(1.2); } .input-area textarea { flex: 1; min-width: 200px; border: 1px solid var(--border-color); border-radius: 30px; padding: 14px 20px; font-size: 0.95rem; resize: none; max-height: 120px; outline: none; background: var(--bg-input); color: var(--text-primary); transition: var(--transition); font-family: inherit; } .input-area textarea:focus { border-color: var(--accent-light); background: var(--bg-tertiary); } .send-btn { background: var(--accent); border: none; color: white; width: 52px; height: 52px; border-radius: 50%; cursor: pointer; font-size: 1.2rem; display: flex; align-items: center; justify-content: center; transition: var(--transition); } .send-btn:hover { background: var(--accent-hover); transform: scale(1.05) rotate(-10deg); } /* Предпросмотр медиа */ .preview-container { width: 100%; padding: 10px 20px 0; display: flex; gap: 10px; flex-wrap: wrap; background: var(--bg-secondary); } .media-preview { max-width: 150px; max-height: 150px; border-radius: 12px; object-fit: cover; border: 2px solid var(--accent); position: relative; } .preview-close { background: var(--danger); color: white; border: none; width: 24px; height: 24px; border-radius: 50%; cursor: pointer; font-size: 0.8rem; display: flex; align-items: center; justify-content: center; margin-left: -30px; margin-top: -10px; z-index: 10; } /* Всплывающие уведомления */ .toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--accent); color: white; padding: 12px 24px; border-radius: 50px; box-shadow: 0 5px 20px rgba(0,0,0,0.3); z-index: 3000; font-weight: 500; display: flex; align-items: center; gap: 10px; opacity: 0; transition: opacity 0.3s; } .toast.show { opacity: 1; } /* Адаптивность */ @media (max-width: 768px) { .sidebar { position: absolute; z-index: 100; height: 100%; box-shadow: 5px 0 20px rgba(0,0,0,0.5); } .sidebar.collapsed { width: 0; } .chat-title-text { font-size: 1rem; } .header h2 { font-size: 1.2rem; } .attach-btn, .send-btn, .emoji-btn { width: 44px; height: 44px; } } .hidden { display: none !important; } </style> </head> <body> <div class="app"> <!-- Шапка --> <div class="header"> <h2><i class="fas fa-comments" style="margin-right: 10px; color: var(--accent-light);"></i>Локальный чат</h2> <div class="user-info hidden" id="userInfo"> <div class="user-avatar" id="userAvatarContainer"> <img id="userAvatarImg" src="" style="display: none;"> <span id="userAvatarText"></span> </div> <span class="user-name" id="userName"></span> <button class="logout-btn" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> Выйти</button> </div> </div> <!-- Экран входа --> <div id="loginScreen" class="login-screen"> <h3><i class="fas fa-lock-open" style="margin-right: 12px;"></i>Вход в чат</h3> <input type="text" id="loginNameInput" class="login-input" placeholder="Ваше имя"> <button id="loginBtn" class="login-btn"><i class="fas fa-user"></i> Войти как обычный пользователь</button> <button id="adminLoginBtn" class="login-btn admin-btn"><i class="fas fa-crown"></i> Войти как администратор</button> </div> <!-- Модальное окно для пароля администратора --> <div id="adminPasswordModal" class="modal hidden"> <div class="modal-content"> <h3><i class="fas fa-crown"></i> Вход администратора</h3> <input type="password" id="adminPasswordInput" placeholder="Пароль администратора"> <button id="adminPasswordSubmit"><i class="fas fa-key"></i> Войти</button> <button id="adminPasswordCancel"><i class="fas fa-times"></i> Отмена</button> </div> </div> <!-- Основное приложение (скрыто до входа) --> <div id="mainApp" class="main hidden"> <!-- Боковая панель --> <div class="sidebar" id="sidebar"> <div class="sidebar-header"> <span><i class="fas fa-comment-dots" style="margin-right: 8px;"></i>Чаты</span> <button id="newChatBtn" class="new-chat-btn" title="Создать чат"><i class="fas fa-plus"></i></button> </div> <!-- Поле поиска --> <div class="search-box"> <i class="fas fa-search"></i> <input type="text" id="searchChats" placeholder="Поиск чатов..."> </div> <div id="chatList" class="chat-list"></div> </div> <!-- Область чата --> <div class="chat-area"> <div class="chat-header"> <div class="chat-header-left"> <button id="toggleSidebarBtn" class="toggle-sidebar" title="Скрыть/показать чаты"><i class="fas fa-bars"></i></button> <div class="chat-title"> <div class="chat-title-avatar" id="chatAvatarContainer" title="Сменить аватарку чата"> <img id="chatAvatarImg" src="" style="display: none;"> <span id="chatAvatarText">👥</span> </div> <div> <div class="chat-title-text" id="currentChatName"></div> <div class="chat-members-count" id="currentChatMembersCount"><i class="fas fa-users"></i> <span></span></div> </div> </div> </div> <button id="addMemberBtn" class="add-member-btn" title="Добавить участника"><i class="fas fa-user-plus"></i></button> </div> <div id="messages" class="messages-area"></div> <!-- Область ввода с кнопками для фото/видео и смайлов --> <div class="preview-container hidden" id="previewContainer"></div> <div class="input-area"> <div class="attach-buttons"> <label class="attach-btn" id="photoLabel" title="Прикрепить фото"><i class="fas fa-camera"></i></label> <input type="file" id="photoInput" accept="image/*" hidden> <label class="attach-btn" id="videoLabel" title="Прикрепить видео"><i class="fas fa-video"></i></label> <input type="file" id="videoInput" accept="video/*" hidden> <button class="emoji-btn" id="emojiBtn" title="Смайлы"><i class="fas fa-smile"></i></button> </div> <!-- Панель эмодзи --> <div id="emojiPanel" class="emoji-panel hidden"> <!-- Эмодзи будут вставлены через JS --> </div> <textarea id="messageText" placeholder="Напишите сообщение..." rows="1"></textarea> <button id="sendBtn" class="send-btn"><i class="fas fa-paper-plane"></i></button> </div> </div> </div> </div> <script> (function() { // ---------- Конфигурация ---------- const ADMIN_PASSWORD = 'admin123'; // пароль администратора // Список популярных эмодзи const EMOJIS = [ '😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩', '🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😬', '🙄', '😯', '😦', '😧', '😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐', '🥴', '🤢', '🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '😈', '👿', '👹', '👺', '🤡', '💩', '👻', '💀', '☠️', '👽', '👾', '🤖', '🎃', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈', '♉' ]; // ---------- Состояние приложения ---------- let currentUser = null; // { name: string, isAdmin: boolean, avatar: string? } let currentChatId = null; let users = []; // массив объектов пользователей { name, avatar? } let chats = []; // массив объектов чатов { id, name, createdBy, members, avatar? } let messages = {}; // объект: id чата -> массив сообщений let selectedFile = null; // выбранный файл (для фото/видео) let selectedFileType = null; // 'image' или 'video' // Загрузка данных из localStorage function loadData() { const storedUsers = localStorage.getItem('users'); if (storedUsers) { users = JSON.parse(storedUsers); } else { users = []; } const storedChats = localStorage.getItem('chats'); if (storedChats) { chats = JSON.parse(storedChats); } else { // Если нет ни одного чата, создаём демо-чат const defaultChat = { id: 'chat_' + Date.now(), name: 'Общий чат', createdBy: 'system', members: [], avatar: null }; chats = [defaultChat]; } const storedMessages = localStorage.getItem('messages'); if (storedMessages) { messages = JSON.parse(storedMessages); } else { messages = {}; chats.forEach(chat => { messages[chat.id] = []; }); } // Добавим демо-сообщение для нового чата, если пусто chats.forEach(chat => { if (!messages[chat.id] || messages[chat.id].length === 0) { messages[chat.id] = [ { id: 1, text: 'Добро пожаловать в чат!', author: 'Система', timestamp: Date.now() - 3600000 } ]; } }); } loadData(); // Сохранение данных в localStorage function saveData() { localStorage.setItem('users', JSON.stringify(users)); localStorage.setItem('chats', JSON.stringify(chats)); localStorage.setItem('messages', JSON.stringify(messages)); } // ---------- Вспомогательные функции ---------- function showToast(message, icon = 'fa-check-circle') { const toast = document.createElement('div'); toast.className = 'toast'; toast.innerHTML = `<i class="fas ${icon}"></i> ${message}`; document.body.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 2000); } function getInitial(name) { return name ? name.charAt(0).toUpperCase() : '?'; } // Получить данные пользователя по имени function getUserData(name) { return users.find(u => u.name === name) || { name, avatar: null }; } // Обновить или создать пользователя function updateUser(user) { const index = users.findIndex(u => u.name === user.name); if (index !== -1) { users[index] = user; } else { users.push(user); } saveData(); } // ---------- Управление аватарками ---------- function readFileAsDataURL(file, callback) { const reader = new FileReader(); reader.onload = (e) => callback(e.target.result); reader.readAsDataURL(file); } // Смена аватарки текущего пользователя function changeUserAvatar() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; readFileAsDataURL(file, (dataUrl) => { const userData = getUserData(currentUser.name); userData.avatar = dataUrl; updateUser(userData); currentUser.avatar = dataUrl; updateUserAvatarDisplay(); showToast('Аватар обновлён', 'fa-check-circle'); }); }; input.click(); } // Смена аватарки текущего чата function changeChatAvatar() { if (!currentChatId) return; const chat = chats.find(c => c.id === currentChatId); if (!chat) return; if (!currentUser.isAdmin && chat.createdBy !== currentUser.name) { alert('Только создатель чата или администратор может менять аватарку.'); return; } const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; readFileAsDataURL(file, (dataUrl) => { chat.avatar = dataUrl; saveData(); updateChatAvatarDisplay(chat); showToast('Аватар чата обновлён', 'fa-check-circle'); }); }; input.click(); } // ---------- Управление медиафайлами (фото/видео) ---------- function handleFileSelect(event, type) { const file = event.target.files[0]; if (!file) return; if ((type === 'image' && !file.type.startsWith('image/')) || (type === 'video' && !file.type.startsWith('video/'))) { alert('Неверный тип файла'); return; } if (file.size > 20 * 1024 * 1024) { alert('Файл слишком большой (макс. 20 МБ)'); return; } selectedFile = file; selectedFileType = type; // Предпросмотр const reader = new FileReader(); reader.onload = (e) => { const previewContainer = document.getElementById('previewContainer'); previewContainer.innerHTML = ''; previewContainer.classList.remove('hidden'); const preview = document.createElement(type === 'image' ? 'img' : 'video'); preview.src = e.target.result; preview.className = 'media-preview'; if (type === 'video') { preview.controls = true; preview.preload = 'metadata'; } const closeBtn = document.createElement('button'); closeBtn.className = 'preview-close'; closeBtn.innerHTML = '<i class="fas fa-times"></i>'; closeBtn.onclick = () => { selectedFile = null; selectedFileType = null; previewContainer.innerHTML = ''; previewContainer.classList.add('hidden'); photoInput.value = ''; videoInput.value = ''; }; previewContainer.appendChild(preview); previewContainer.appendChild(closeBtn); }; reader.readAsDataURL(file); } function clearMediaPreview() { selectedFile = null; selectedFileType = null; const previewContainer = document.getElementById('previewContainer'); previewContainer.innerHTML = ''; previewContainer.classList.add('hidden'); photoInput.value = ''; videoInput.value = ''; } // ---------- Работа с эмодзи ---------- function initEmojiPanel() { const panel = document.getElementById('emojiPanel'); EMOJIS.forEach(emoji => { const item = document.createElement('span'); item.className = 'emoji-item'; item.textContent = emoji; item.addEventListener('click', () => { insertEmoji(emoji); }); panel.appendChild(item); }); } function insertEmoji(emoji) { const textarea = document.getElementById('messageText'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; const newText = text.substring(0, start) + emoji + text.substring(end); textarea.value = newText; textarea.focus(); textarea.selectionStart = textarea.selectionEnd = start + emoji.length; } // ---------- Интерфейс ---------- const loginScreen = document.getElementById('loginScreen'); const mainApp = document.getElementById('mainApp'); const userInfo = document.getElementById('userInfo'); const userNameSpan = document.getElementById('userName'); const userAvatarContainer = document.getElementById('userAvatarContainer'); const userAvatarImg = document.getElementById('userAvatarImg'); const userAvatarText = document.getElementById('userAvatarText'); const logoutBtn = document.getElementById('logoutBtn'); const loginNameInput = document.getElementById('loginNameInput'); const loginBtn = document.getElementById('loginBtn'); const adminLoginBtn = document.getElementById('adminLoginBtn'); const adminPasswordModal = document.getElementById('adminPasswordModal'); const adminPasswordInput = document.getElementById('adminPasswordInput'); const adminPasswordSubmit = document.getElementById('adminPasswordSubmit'); const adminPasswordCancel = document.getElementById('adminPasswordCancel'); const sidebar = document.getElementById('sidebar'); const toggleSidebarBtn = document.getElementById('toggleSidebarBtn'); const newChatBtn = document.getElementById('newChatBtn'); const searchChatsInput = document.getElementById('searchChats'); const chatListDiv = document.getElementById('chatList'); const currentChatNameSpan = document.getElementById('currentChatName'); const currentChatMembersCountSpan = document.getElementById('currentChatMembersCount').querySelector('span'); const chatAvatarContainer = document.getElementById('chatAvatarContainer'); const chatAvatarImg = document.getElementById('chatAvatarImg'); const chatAvatarText = document.getElementById('chatAvatarText'); const addMemberBtn = document.getElementById('addMemberBtn'); const messagesDiv = document.getElementById('messages'); const messageText = document.getElementById('messageText'); const sendBtn = document.getElementById('sendBtn'); const photoInput = document.getElementById('photoInput'); const videoInput = document.getElementById('videoInput'); const photoLabel = document.getElementById('photoLabel'); const videoLabel = document.getElementById('videoLabel'); const emojiBtn = document.getElementById('emojiBtn'); const emojiPanel = document.getElementById('emojiPanel'); // Инициализация панели эмодзи initEmojiPanel(); // Открытие/закрытие панели эмодзи emojiBtn.addEventListener('click', (e) => { e.stopPropagation(); emojiPanel.classList.toggle('hidden'); }); // Закрытие панели при клике вне (упрощённо — при клике на другие элементы, но мы не реализуем сложную логику) document.addEventListener('click', (e) => { if (!emojiBtn.contains(e.target) && !emojiPanel.contains(e.target)) { emojiPanel.classList.add('hidden'); } }); // Состояние боковой панели let sidebarCollapsed = localStorage.getItem('sidebar_collapsed') === 'true'; function updateSidebar() { if (sidebarCollapsed) { sidebar.classList.add('collapsed'); } else { sidebar.classList.remove('collapsed'); } localStorage.setItem('sidebar_collapsed', sidebarCollapsed); } toggleSidebarBtn.addEventListener('click', () => { sidebarCollapsed = !sidebarCollapsed; updateSidebar(); }); updateSidebar(); // Клик по аватарке пользователя userAvatarContainer.addEventListener('click', (e) => { e.stopPropagation(); if (currentUser) changeUserAvatar(); }); // Клик по аватарке чата chatAvatarContainer.addEventListener('click', (e) => { e.stopPropagation(); changeChatAvatar(); }); // Обработчики загрузки фото/видео photoLabel.addEventListener('click', () => photoInput.click()); videoLabel.addEventListener('click', () => videoInput.click()); photoInput.addEventListener('change', (e) => handleFileSelect(e, 'image')); videoInput.addEventListener('change', (e) => handleFileSelect(e, 'video')); // Вход обычного пользователя function loginAsUser(name) { if (!name.trim()) { alert('Введите имя'); return false; } if (name === 'Администратор') { alert('Это имя зарезервировано для администратора. Используйте вход для администратора.'); return false; } let userData = getUserData(name); if (!userData) { userData = { name, avatar: null }; users.push(userData); } currentUser = { name, isAdmin: false, avatar: userData.avatar }; afterLogin(); return true; } // Вход администратора (с паролем) function loginAsAdmin(password) { if (password !== ADMIN_PASSWORD) { alert('Неверный пароль'); return false; } const adminName = 'Администратор'; let userData = getUserData(adminName); if (!userData) { userData = { name: adminName, avatar: null }; users.push(userData); } currentUser = { name: adminName, isAdmin: true, avatar: userData.avatar }; afterLogin(); return true; } function afterLogin() { localStorage.setItem('currentUser', JSON.stringify(currentUser)); loginScreen.classList.add('hidden'); mainApp.classList.remove('hidden'); userInfo.classList.remove('hidden'); userNameSpan.textContent = currentUser.name; updateUserAvatarDisplay(); renderChatList(); const savedChatId = localStorage.getItem('current_chat'); if (savedChatId && chats.some(c => c.id === savedChatId)) { switchChat(savedChatId); } else if (chats.length > 0) { switchChat(chats[0].id); } else { createDefaultChat(); } saveData(); } function updateUserAvatarDisplay() { if (currentUser.avatar) { userAvatarImg.src = currentUser.avatar; userAvatarImg.style.display = 'block'; userAvatarText.style.display = 'none'; } else { userAvatarImg.style.display = 'none'; userAvatarText.style.display = 'flex'; userAvatarText.textContent = getInitial(currentUser.name); } if (currentUser.isAdmin) { userAvatarContainer.style.backgroundColor = 'var(--danger)'; } else { userAvatarContainer.style.backgroundColor = 'var(--accent)'; } } function updateChatAvatarDisplay(chat) { if (chat.avatar) { chatAvatarImg.src = chat.avatar; chatAvatarImg.style.display = 'block'; chatAvatarText.style.display = 'none'; } else { chatAvatarImg.style.display = 'none'; chatAvatarText.style.display = 'flex'; chatAvatarText.textContent = getInitial(chat.name); } } function createDefaultChat() { const newChat = { id: 'chat_' + Date.now(), name: 'Общий чат', createdBy: 'system', members: [], avatar: null }; chats.push(newChat); messages[newChat.id] = []; switchChat(newChat.id); saveData(); } loginBtn.addEventListener('click', () => { const name = loginNameInput.value.trim(); if (name) loginAsUser(name); }); adminLoginBtn.addEventListener('click', () => { adminPasswordModal.classList.remove('hidden'); adminPasswordInput.value = ''; adminPasswordInput.focus(); }); adminPasswordSubmit.addEventListener('click', () => { const password = adminPasswordInput.value.trim(); if (loginAsAdmin(password)) { adminPasswordModal.classList.add('hidden'); } }); adminPasswordCancel.addEventListener('click', () => { adminPasswordModal.classList.add('hidden'); }); adminPasswordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { adminPasswordSubmit.click(); } }); logoutBtn.addEventListener('click', () => { localStorage.removeItem('currentUser'); localStorage.removeItem('current_chat'); currentUser = null; currentChatId = null; loginScreen.classList.remove('hidden'); mainApp.classList.add('hidden'); userInfo.classList.add('hidden'); }); // ---------- Работа с чатами (с поиском) ---------- function renderChatList(filterText = '') { if (!chats.length) { chatListDiv.innerHTML = '<div class="chat-item" style="color: var(--text-secondary);"><i class="fas fa-inbox"></i> Нет чатов. Создайте первый.</div>'; return; } chatListDiv.innerHTML = ''; const filtered = chats.filter(chat => { if (!currentUser.isAdmin && chat.members && !chat.members.includes(currentUser.name) && chat.createdBy !== currentUser.name) { return false; } if (filterText) { return chat.name.toLowerCase().includes(filterText.toLowerCase()); } return true; }); if (filtered.length === 0) { chatListDiv.innerHTML = '<div class="chat-item" style="color: var(--text-secondary);"><i class="fas fa-search"></i> Ничего не найдено</div>'; return; } filtered.forEach(chat => { const item = document.createElement('div'); item.className = 'chat-item'; if (chat.id === currentChatId) item.classList.add('active'); const avatarDiv = document.createElement('div'); avatarDiv.className = 'chat-avatar'; if (chat.avatar) { const img = document.createElement('img'); img.src = chat.avatar; avatarDiv.appendChild(img); } else { avatarDiv.textContent = getInitial(chat.name); } item.appendChild(avatarDiv); const infoDiv = document.createElement('div'); infoDiv.className = 'chat-info'; infoDiv.innerHTML = ` <div class="chat-name">${chat.name}</div> <div class="chat-meta"> <span><i class="fas fa-users"></i> ${chat.members ? chat.members.length : 1}</span> <span>${getLastMessageText(chat.id)}</span> </div> `; item.appendChild(infoDiv); if (currentUser.isAdmin || chat.createdBy === currentUser.name) { const actions = document.createElement('div'); actions.className = 'chat-actions'; actions.innerHTML = ` <button class="chat-action-btn" title="Удалить чат"><i class="fas fa-trash-alt"></i></button> `; actions.querySelector('button').addEventListener('click', (e) => { e.stopPropagation(); deleteChat(chat.id); }); item.appendChild(actions); } item.addEventListener('click', () => switchChat(chat.id)); chatListDiv.appendChild(item); }); } function getLastMessageText(chatId) { const msgs = messages[chatId]; if (!msgs || msgs.length === 0) return ''; const last = msgs[msgs.length - 1]; if (last.text) return last.text.substring(0, 20) + (last.text.length > 20 ? '…' : ''); if (last.photo) return '📷 Фото'; if (last.video) return '🎬 Видео'; return ''; } function switchChat(chatId) { if (chatId === currentChatId) return; currentChatId = chatId; localStorage.setItem('current_chat', chatId); const chat = chats.find(c => c.id === chatId); if (!chat) return; currentChatNameSpan.textContent = chat.name; currentChatMembersCountSpan.textContent = chat.members ? chat.members.length : 1; updateChatAvatarDisplay(chat); renderMessages(chatId); renderChatList(searchChatsInput.value); } function renderMessages(chatId) { const msgs = messages[chatId] || []; messagesDiv.innerHTML = ''; msgs.sort((a,b) => a.timestamp - b.timestamp).forEach(msg => { const isOwn = msg.author === currentUser.name; const row = document.createElement('div'); row.className = 'message-row'; if (isOwn) row.classList.add('own'); if (!isOwn) { const authorData = getUserData(msg.author); const avatar = document.createElement('div'); avatar.className = 'message-avatar'; if (authorData && authorData.avatar) { const img = document.createElement('img'); img.src = authorData.avatar; avatar.appendChild(img); } else { avatar.textContent = getInitial(msg.author); avatar.style.backgroundColor = 'var(--accent-light)'; } row.appendChild(avatar); } const bubble = document.createElement('div'); bubble.className = 'message-bubble'; if (isOwn) bubble.classList.add('own'); const header = document.createElement('div'); header.className = 'message-header'; header.innerHTML = ` <span class="message-author">${msg.author}</span> <span class="message-time"><i class="far fa-clock"></i> ${new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span> `; bubble.appendChild(header); if (msg.text) { const textDiv = document.createElement('div'); textDiv.className = 'message-text'; textDiv.textContent = msg.text; bubble.appendChild(textDiv); } if (msg.photo) { const img = document.createElement('img'); img.className = 'message-media'; img.src = msg.photo; img.addEventListener('click', () => window.open(msg.photo)); bubble.appendChild(img); } if (msg.video) { const video = document.createElement('video'); video.className = 'message-video'; video.src = msg.video; video.controls = true; video.preload = 'metadata'; bubble.appendChild(video); } if (isOwn || currentUser.isAdmin) { const actions = document.createElement('div'); actions.className = 'message-actions'; actions.innerHTML = ` ${isOwn ? '<span class="message-action" title="Редактировать"><i class="fas fa-edit"></i></span>' : ''} <span class="message-action" title="Удалить"><i class="fas fa-trash-alt"></i></span> `; actions.querySelector('[title="Удалить"]').addEventListener('click', (e) => { e.stopPropagation(); deleteMessage(chatId, msg.id); }); if (isOwn) { actions.querySelector('[title="Редактировать"]').addEventListener('click', (e) => { e.stopPropagation(); editMessage(chatId, msg.id, msg.text); }); } bubble.appendChild(actions); } row.appendChild(bubble); messagesDiv.appendChild(row); }); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function deleteMessage(chatId, messageId) { if (!confirm('Удалить сообщение?')) return; const msgs = messages[chatId]; if (!msgs) return; const index = msgs.findIndex(m => m.id === messageId); if (index !== -1) { msgs.splice(index, 1); saveData(); renderMessages(chatId); showToast('Сообщение удалено', 'fa-trash-alt'); } } function editMessage(chatId, messageId, oldText) { const newText = prompt('Редактировать сообщение:', oldText); if (newText === null || newText.trim() === '') return; const msgs = messages[chatId]; const msg = msgs.find(m => m.id === messageId); if (msg) { msg.text = newText.trim(); saveData(); renderMessages(chatId); showToast('Сообщение изменено', 'fa-edit'); } } function deleteChat(chatId) { if (!confirm('Удалить чат? Это действие необратимо.')) return; const index = chats.findIndex(c => c.id === chatId); if (index !== -1) { chats.splice(index, 1); delete messages[chatId]; saveData(); if (currentChatId === chatId) { if (chats.length > 0) { switchChat(chats[0].id); } else { currentChatId = null; currentChatNameSpan.textContent = ''; currentChatMembersCountSpan.textContent = ''; messagesDiv.innerHTML = ''; } } renderChatList(searchChatsInput.value); showToast('Чат удалён', 'fa-trash-alt'); } } newChatBtn.addEventListener('click', () => { const name = prompt('Введите название нового чата:'); if (!name || !name.trim()) return; const newChat = { id: 'chat_' + Date.now() + Math.random(), name: name.trim(), createdBy: currentUser.name, members: [currentUser.name], avatar: null }; chats.push(newChat); messages[newChat.id] = []; saveData(); switchChat(newChat.id); showToast('Чат создан', 'fa-check-circle'); }); addMemberBtn.addEventListener('click', () => { const chat = chats.find(c => c.id === currentChatId); if (!chat) return; if (!currentUser.isAdmin && chat.createdBy !== currentUser.name) { alert('Только создатель или администратор может добавлять участников.'); return; } const available = users.filter(u => !chat.members.includes(u.name) && u.name !== currentUser.name).map(u => u.name); if (available.length === 0) { alert('Нет доступных пользователей'); return; } const name = prompt('Введите имя участника:\n' + available.join(', ')); if (!name) return; if (!available.includes(name)) { alert('Пользователь не найден'); return; } chat.members.push(name); saveData(); currentChatMembersCountSpan.textContent = chat.members.length; showToast(`${name} добавлен в чат`, 'fa-user-plus'); }); // Отправка сообщения (с учётом медиа) function sendMessage() { if (!currentChatId) return; const text = messageText.value.trim(); if (!text && !selectedFile) return; const msg = { id: Date.now() + Math.random(), author: currentUser.name, timestamp: Date.now() }; if (text) msg.text = text; if (selectedFile) { const reader = new FileReader(); reader.onload = (e) => { if (selectedFileType === 'image') { msg.photo = e.target.result; } else if (selectedFileType === 'video') { msg.video = e.target.result; } if (!messages[currentChatId]) messages[currentChatId] = []; messages[currentChatId].push(msg); saveData(); renderMessages(currentChatId); messageText.value = ''; clearMediaPreview(); }; reader.readAsDataURL(selectedFile); } else { if (!messages[currentChatId]) messages[currentChatId] = []; messages[currentChatId].push(msg); saveData(); renderMessages(currentChatId); messageText.value = ''; } } sendBtn.addEventListener('click', sendMessage); messageText.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Поиск чатов searchChatsInput.addEventListener('input', (e) => { renderChatList(e.target.value); }); // Автовход при наличии сессии const savedUser = localStorage.getItem('currentUser'); if (savedUser) { try { const parsed = JSON.parse(savedUser); if (parsed && parsed.name) { currentUser = parsed; afterLogin(); } else { loginScreen.classList.remove('hidden'); } } catch { loginScreen.classList.remove('hidden'); } } else { loginScreen.classList.remove('hidden'); } })(); </script> </body> </html>