na-kakuyu-temu-mozhno-sozdat/app.js

474 lines
14 KiB
JavaScript

// VibeChat Messenger App
const API = {
me: '/api/auth/me',
logout: '/api/auth/logout',
users: '/api/users',
chats: '/api/chats',
updateProfile: '/api/users/me'
};
let currentUser = null;
let currentChat = null;
let allChats = [];
let allUsers = [];
// Initialize app
window.addEventListener('DOMContentLoaded', async () => {
const token = localStorage.getItem('vibechat_token');
if (!token) {
window.location.href = 'index.html';
return;
}
await loadCurrentUser();
await loadChats();
await loadUsers();
// Inject emoji picker HTML
const emojis = ['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'];
document.getElementById('emojiPicker').innerHTML = `
<div class="emoji-grid">
${emojis.map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
</div>
`;
});
// Load current user
async function loadCurrentUser() {
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.me, {
headers: { 'Authorization': `Bearer ${token}` }
});
const json = await res.json();
if (!json.ok) {
localStorage.removeItem('vibechat_token');
window.location.href = 'index.html';
return;
}
currentUser = json.user;
localStorage.setItem('vibechat_user', JSON.stringify(currentUser));
// Update UI
document.getElementById('currentUserName').textContent = currentUser.username;
document.getElementById('currentUserAvatar').src = currentUser.avatar;
} catch (e) {
console.error('Failed to load user:', e);
}
}
// Load chats
async function loadChats() {
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.chats, {
headers: { 'Authorization': `Bearer ${token}` }
});
const json = await res.json();
if (json.ok) {
allChats = json.chats;
renderChatList();
}
} catch (e) {
console.error('Failed to load chats:', e);
}
}
// Render chat list
function renderChatList() {
const chatList = document.getElementById('chatList');
if (allChats.length === 0) {
chatList.innerHTML = `
<div style="padding:40px 20px;text-align:center;color:#86868b">
<p>Нет чатов</p>
<p style="font-size:13px;margin-top:8px">Нажмите + чтобы создать новый</p>
</div>
`;
return;
}
chatList.innerHTML = allChats.map(chat => `
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-item-name">
${chat.name}
${chat.type !== 'private' ? `<span class="chat-type-badge">${chat.type === 'group' ? 'Группа' : 'Канал'}</span>` : ''}
</div>
<div class="chat-item-last">
${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}
</div>
</div>
${chat.lastMessage ? `<span class="chat-item-time">${formatTime(chat.lastMessage.createdAt)}</span>` : ''}
</div>
`).join('');
}
// Load users for new chat
async function loadUsers() {
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.users, {
headers: { 'Authorization': `Bearer ${token}` }
});
const json = await res.json();
if (json.ok) {
allUsers = json.users;
renderUserList();
}
} catch (e) {
console.error('Failed to load users:', e);
}
}
// Render user list
function renderUserList() {
const userList = document.getElementById('userList');
if (allUsers.length === 0) {
userList.innerHTML = '<p style="padding:20px;text-align:center;color:#86868b">Нет пользователей</p>';
return;
}
userList.innerHTML = allUsers.map(user => `
<div class="user-item" onclick="createPrivateChat(${user.id})">
<img src="${user.avatar}" alt="" class="user-item-avatar">
<span class="user-item-name">${user.username}</span>
</div>
`).join('');
}
// Select chat
async function selectChat(chatId) {
currentChat = allChats.find(c => c.id === chatId);
if (!currentChat) return;
// Update UI
document.getElementById('emptyChat').style.display = 'none';
document.getElementById('messagesList').style.display = 'flex';
document.getElementById('messageInputContainer').style.display = 'block';
// Update header
document.getElementById('chatHeaderName').textContent = currentChat.name;
document.getElementById('chatHeaderAvatar').src = currentChat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + currentChat.id;
document.getElementById('chatHeaderStatus').textContent = currentChat.type === 'channel' ? 'Канал' : (currentChat.type === 'group' ? `${currentChat.participants?.length || 0} участников` : 'Онлайн');
// Render chat list again to update active state
renderChatList();
// Load messages
await loadMessages(chatId);
}
// Load messages
async function loadMessages(chatId) {
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(`${API.chats}/${chatId}/messages`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const json = await res.json();
if (json.ok) {
renderMessages(json.messages);
}
} catch (e) {
console.error('Failed to load messages:', e);
}
}
// Render messages
function renderMessages(messages) {
const messagesList = document.getElementById('messagesList');
if (messages.length === 0) {
messagesList.innerHTML = '<p style="text-align:center;color:#86868b;padding:40px">Сообщений пока нет</p>';
return;
}
messagesList.innerHTML = messages.map(msg => `
<div class="message ${msg.senderId === currentUser.id ? 'sent' : ''}">
<img src="${msg.senderAvatar}" alt="" class="message-avatar">
<div class="message-content">
${msg.senderId !== currentUser.id ? `<div class="message-sender">${msg.senderName}</div>` : ''}
<div class="message-text">${escapeHtml(msg.text)}</div>
<div class="message-time">${formatTime(msg.createdAt)}</div>
</div>
</div>
`).join('');
// Scroll to bottom
const container = document.getElementById('messagesContainer');
container.scrollTop = container.scrollHeight;
}
// Send message
async function sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (!text || !currentChat) return;
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(`${API.chats}/${currentChat.id}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ text })
});
const json = await res.json();
if (json.ok) {
input.value = '';
await loadMessages(currentChat.id);
await loadChats(); // Update last message
} else {
alert('Ошибка отправки: ' + json.error);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
// Handle enter key
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
// Emoji picker
function toggleEmojiPicker() {
const picker = document.getElementById('emojiPicker');
picker.classList.toggle('active');
}
function insertEmoji(emoji) {
const input = document.getElementById('messageInput');
input.value += emoji;
input.focus();
document.getElementById('emojiPicker').classList.remove('active');
}
// Close emoji picker on outside click
document.addEventListener('click', (e) => {
const picker = document.getElementById('emojiPicker');
const btn = document.querySelector('.btn-emoji');
if (!picker.contains(e.target) && !btn.contains(e.target)) {
picker.classList.remove('active');
}
});
// New chat modal
function showNewChatModal() {
document.getElementById('newChatModal').classList.add('active');
loadUsers();
}
function closeNewChatModal() {
document.getElementById('newChatModal').classList.remove('active');
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('chatTab').style.display = tab === 'chat' ? 'block' : 'none';
document.getElementById('groupTab').style.display = tab === 'group' ? 'block' : 'none';
document.getElementById('channelTab').style.display = tab === 'channel' ? 'block' : 'none';
}
// Create private chat
async function createPrivateChat(userId) {
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.chats, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ type: 'private', participantId: userId })
});
const json = await res.json();
if (json.ok) {
closeNewChatModal();
await loadChats();
selectChat(json.chat.id);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
// Create group
async function createGroup() {
const name = document.getElementById('groupName').value.trim();
if (!name) return alert('Введите название группы');
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.chats, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ type: 'group', name })
});
const json = await res.json();
if (json.ok) {
closeNewChatModal();
document.getElementById('groupName').value = '';
await loadChats();
selectChat(json.chat.id);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
// Create channel
async function createChannel() {
const name = document.getElementById('channelName').value.trim();
if (!name) return alert('Введите название канала');
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.chats, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ type: 'channel', name })
});
const json = await res.json();
if (json.ok) {
closeNewChatModal();
document.getElementById('channelName').value = '';
await loadChats();
selectChat(json.chat.id);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
// Settings
function openSettings() {
const user = JSON.parse(localStorage.getItem('vibechat_user'));
document.getElementById('settingsAvatar').src = user.avatar;
document.getElementById('settingsUsername').value = user.username;
document.getElementById('settingsBio').value = user.bio || '';
document.getElementById('settingsModal').classList.add('active');
}
function closeSettings() {
document.getElementById('settingsModal').classList.remove('active');
}
async function updateProfile(event) {
event.preventDefault();
const username = document.getElementById('settingsUsername').value;
const bio = document.getElementById('settingsBio').value;
try {
const token = localStorage.getItem('vibechat_token');
const res = await fetch(API.updateProfile, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ username, bio })
});
const json = await res.json();
if (json.ok) {
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
currentUser = json.user;
document.getElementById('currentUserName').textContent = currentUser.username;
document.getElementById('currentUserAvatar').src = currentUser.avatar;
closeSettings();
alert('Профиль обновлён! ✅');
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
function changeAvatar() {
const url = prompt('Введите URL аватара:', currentUser.avatar);
if (url) {
updateProfile({ preventDefault: () => {} });
}
}
// Logout
async function logout() {
if (!confirm('Вы уверены, что хотите выйти?')) return;
try {
const token = localStorage.getItem('vibechat_token');
await fetch(API.logout, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
} catch (e) {}
localStorage.removeItem('vibechat_token');
localStorage.removeItem('vibechat_user');
window.location.href = 'index.html';
}
// Filter chats
function filterChats() {
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = allChats.filter(c => c.name.toLowerCase().includes(query));
const chatList = document.getElementById('chatList');
chatList.innerHTML = filtered.map(chat => `
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
<div class="chat-item-info">
<div class="chat-item-name">${chat.name}</div>
<div class="chat-item-last">${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}</div>
</div>
</div>
`).join('');
}
// Helper functions
function formatTime(isoString) {
const date = new Date(isoString);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Только что';
if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад';
if (diff < 86400000) return Math.floor(diff / 3600000) + ' ч назад';
if (diff < 604800000) return Math.floor(diff / 86400000) + ' дн назад';
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}