Kazakhtelecom: Modern Liquid Glass website with all services, tariffs, bundles
This commit is contained in:
parent
e5deccd85e
commit
e09fb66b25
689
app.js
689
app.js
@ -1,473 +1,332 @@
|
||||
// VibeChat Messenger App
|
||||
// Kazakhtelecom Website — JavaScript
|
||||
|
||||
const API = {
|
||||
me: '/api/auth/me',
|
||||
logout: '/api/auth/logout',
|
||||
users: '/api/users',
|
||||
chats: '/api/chats',
|
||||
updateProfile: '/api/users/me'
|
||||
};
|
||||
const API = '/api/data';
|
||||
let siteData = null;
|
||||
let currentTariffCategory = 'internet';
|
||||
|
||||
let currentUser = null;
|
||||
let currentChat = null;
|
||||
let allChats = [];
|
||||
let allUsers = [];
|
||||
|
||||
// Initialize app
|
||||
// Initialize
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
if (!token) {
|
||||
window.location.href = 'index.html';
|
||||
await loadData();
|
||||
renderHero();
|
||||
renderServices();
|
||||
renderTariffs('internet');
|
||||
renderBundles();
|
||||
renderNews();
|
||||
renderFAQ();
|
||||
setupAnimations();
|
||||
setupTariffTabs();
|
||||
setupContactForm();
|
||||
});
|
||||
|
||||
// Load data from JSON
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await fetch(API);
|
||||
const json = await res.json();
|
||||
if (json.ok) {
|
||||
siteData = json.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Render Hero
|
||||
function renderHero() {
|
||||
const { heroTitle, heroSubtitle, heroDescription } = siteData.siteSettings;
|
||||
document.getElementById('heroTitle').textContent = heroTitle;
|
||||
document.getElementById('heroSubtitle').textContent = heroSubtitle;
|
||||
document.getElementById('heroDescription').textContent = heroDescription;
|
||||
}
|
||||
|
||||
// Render Services
|
||||
function renderServices() {
|
||||
const grid = document.getElementById('servicesGrid');
|
||||
grid.innerHTML = siteData.services.map(service => `
|
||||
<div class="service-card" style="--service-color: ${service.color}">
|
||||
<span class="service-icon">${service.icon}</span>
|
||||
<h3>${service.name}</h3>
|
||||
<p>${service.description}</p>
|
||||
<div class="service-features">
|
||||
${service.features.map(f => `<div class="service-feature">${f}</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Render Tariffs
|
||||
function renderTariffs(category) {
|
||||
currentTariffCategory = category;
|
||||
const grid = document.getElementById('tariffsGrid');
|
||||
const tariffs = siteData.tariffs[category] || [];
|
||||
|
||||
if (tariffs.length === 0) {
|
||||
grid.innerHTML = '<p style="text-align:center;color:var(--text-secondary);grid-column:1/-1">Тарифы загружаются...</p>';
|
||||
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();
|
||||
grid.innerHTML = tariffs.map(tariff => {
|
||||
const speedOrChannels = tariff.speed || tariff.channels;
|
||||
const priceLabel = category === 'smart' ? '/мес' : '';
|
||||
|
||||
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>
|
||||
return `
|
||||
<div class="tariff-card ${tariff.popular ? 'popular' : ''}">
|
||||
<h3 class="tariff-name">${tariff.name}</h3>
|
||||
<div class="tariff-speed">${speedOrChannels}</div>
|
||||
<div class="tariff-price">${tariff.price.toLocaleString()} ${tariff.currency}${priceLabel}</div>
|
||||
<div class="tariff-features">
|
||||
${tariff.features.map(f => `<div class="tariff-feature">${f}</div>`).join('')}
|
||||
</div>
|
||||
<button class="btn-tariff" onclick="selectTariff('${tariff.id}')">Подключить</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
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 : 'Нет сообщений'}
|
||||
// Setup Tariff Tabs
|
||||
function setupTariffTabs() {
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
renderTariffs(tab.dataset.category);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Render Bundles
|
||||
function renderBundles() {
|
||||
const grid = document.getElementById('bundlesGrid');
|
||||
grid.innerHTML = siteData.bundles.map(bundle => `
|
||||
<div class="bundle-card ${bundle.popular ? 'popular' : ''}">
|
||||
<h3 class="bundle-name">${bundle.name}</h3>
|
||||
<p class="bundle-description">${bundle.description}</p>
|
||||
<div class="bundle-pricing">
|
||||
<div class="bundle-original">${bundle.originalPrice.toLocaleString()} ₸</div>
|
||||
<div class="bundle-price">
|
||||
${bundle.bundlePrice.toLocaleString()} ₸
|
||||
<span class="bundle-discount">-${bundle.discount}%</span>
|
||||
</div>
|
||||
</div>
|
||||
${chat.lastMessage ? `<span class="chat-item-time">${formatTime(chat.lastMessage.createdAt)}</span>` : ''}
|
||||
<div class="bundle-includes">
|
||||
${bundle.includes.map(item => `<div class="bundle-item">${item}</div>`).join('')}
|
||||
</div>
|
||||
<button class="btn-tariff" onclick="selectBundle('${bundle.id}')">Выбрать пакет</button>
|
||||
</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>
|
||||
// Render News
|
||||
function renderNews() {
|
||||
const grid = document.getElementById('newsGrid');
|
||||
grid.innerHTML = siteData.news.map(news => `
|
||||
<div class="news-card">
|
||||
<img src="${news.image}" alt="${news.title}" class="news-image">
|
||||
<div class="news-content">
|
||||
<div class="news-meta">
|
||||
<span class="news-category">${news.category}</span>
|
||||
<span>${formatDate(news.date)}</span>
|
||||
</div>
|
||||
<h3>${news.title}</h3>
|
||||
<p>${news.excerpt}</p>
|
||||
</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;
|
||||
// Render FAQ
|
||||
function renderFAQ() {
|
||||
const container = document.getElementById('faqContainer');
|
||||
container.innerHTML = siteData.faq.map((item, index) => `
|
||||
<div class="faq-item">
|
||||
<button class="faq-question" onclick="toggleFaq(${index})">
|
||||
${item.question}
|
||||
<span class="faq-icon">+</span>
|
||||
</button>
|
||||
<div class="faq-answer">
|
||||
<p>${item.answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
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
|
||||
// Toggle FAQ
|
||||
function toggleFaq(index) {
|
||||
const items = document.querySelectorAll('.faq-item');
|
||||
items.forEach((item, i) => {
|
||||
if (i === index) {
|
||||
item.classList.toggle('active');
|
||||
} else {
|
||||
alert('Ошибка отправки: ' + json.error);
|
||||
item.classList.remove('active');
|
||||
}
|
||||
} 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();
|
||||
// Contact Form
|
||||
function setupContactForm() {
|
||||
// Phone mask
|
||||
const phoneInput = document.getElementById('contactPhone');
|
||||
phoneInput.addEventListener('input', (e) => {
|
||||
let value = e.target.value.replace(/\D/g, '');
|
||||
if (value.length > 11) value = value.slice(0, 11);
|
||||
|
||||
if (json.ok) {
|
||||
closeNewChatModal();
|
||||
await loadChats();
|
||||
selectChat(json.chat.id);
|
||||
if (value.length > 0) {
|
||||
value = '+7 (' + value.slice(1, 4);
|
||||
}
|
||||
if (value.length > 6) {
|
||||
value = value.slice(0, 6) + ') ' + value.slice(6, 9);
|
||||
}
|
||||
if (value.length > 11) {
|
||||
value = value.slice(0, 11) + '-' + value.slice(11, 13);
|
||||
}
|
||||
if (value.length > 14) {
|
||||
value = value.slice(0, 14) + '-' + value.slice(14, 16);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
e.target.value = value;
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
async function submitContact(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('settingsUsername').value;
|
||||
const bio = document.getElementById('settingsBio').value;
|
||||
const name = document.getElementById('contactName').value;
|
||||
const phone = document.getElementById('contactPhone').value;
|
||||
const service = document.getElementById('contactService').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 res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, phone, service })
|
||||
});
|
||||
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('Профиль обновлён! ✅');
|
||||
alert('✅ ' + json.message);
|
||||
document.getElementById('contactForm').reset();
|
||||
} else {
|
||||
alert('Ошибка: ' + json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
alert('Ошибка отправки: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function changeAvatar() {
|
||||
const url = prompt('Введите URL аватара:', currentUser.avatar);
|
||||
if (url) {
|
||||
updateProfile({ preventDefault: () => {} });
|
||||
// Select tariff
|
||||
function selectTariff(tariffId) {
|
||||
const tariff = siteData.tariffs[currentTariffCategory].find(t => t.id === tariffId);
|
||||
if (!tariff) return;
|
||||
|
||||
const service = currentTariffCategory === 'tv' ? 'Цифровое ТВ' :
|
||||
currentTariffCategory === 'mobile' ? 'Мобильная связь' :
|
||||
currentTariffCategory === 'smart' ? 'Умный дом' : 'Домашний интернет';
|
||||
|
||||
document.getElementById('contactService').value = currentTariffCategory;
|
||||
document.getElementById('contactName').focus();
|
||||
|
||||
alert(`Вы выбрали тариф "${tariff.name}" за ${tariff.price} ₸\n\nЗаполните форму ниже, и мы свяжемся с вами для подключения!`);
|
||||
|
||||
scrollToSection('contacts');
|
||||
}
|
||||
|
||||
// Select bundle
|
||||
function selectBundle(bundleId) {
|
||||
const bundle = siteData.bundles.find(b => b.id === bundleId);
|
||||
if (!bundle) return;
|
||||
|
||||
document.getElementById('contactService').value = 'bundle';
|
||||
document.getElementById('contactName').focus();
|
||||
|
||||
alert(`Вы выбрали пакет "${bundle.name}" за ${bundle.bundlePrice} ₸ (экономия ${bundle.discount}%)\n\nЗаполните форму ниже для подключения!`);
|
||||
|
||||
scrollToSection('contacts');
|
||||
}
|
||||
|
||||
// Scroll to section
|
||||
function scrollToSection(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
if (!confirm('Вы уверены, что хотите выйти?')) return;
|
||||
// Setup animations
|
||||
function setupAnimations() {
|
||||
// Nav scroll effect
|
||||
window.addEventListener('scroll', () => {
|
||||
const nav = document.getElementById('nav');
|
||||
if (window.scrollY > 50) {
|
||||
nav.classList.add('scrolled');
|
||||
} else {
|
||||
nav.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
await fetch(API.logout, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
// Animate stats
|
||||
const stats = document.querySelectorAll('.stat-number');
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const target = parseInt(entry.target.dataset.target);
|
||||
animateNumber(entry.target, target);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
localStorage.removeItem('vibechat_token');
|
||||
localStorage.removeItem('vibechat_user');
|
||||
window.location.href = 'index.html';
|
||||
stats.forEach(stat => observer.observe(stat));
|
||||
|
||||
// Reveal on scroll
|
||||
const reveals = document.querySelectorAll('.service-card, .tariff-card, .bundle-card, .news-card');
|
||||
const revealObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
reveals.forEach(el => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateY(30px)';
|
||||
el.style.transition = 'all 0.6s cubic-bezier(0.25, 1, 0.5, 1)';
|
||||
revealObserver.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// 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('');
|
||||
// Animate number
|
||||
function animateNumber(element, target) {
|
||||
const duration = 2000;
|
||||
const start = 0;
|
||||
const increment = target / (duration / 16);
|
||||
let current = start;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
element.textContent = formatNumber(target);
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
element.textContent = formatNumber(Math.floor(current));
|
||||
}
|
||||
}, 16);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatTime(isoString) {
|
||||
// Format number with spaces
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(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' });
|
||||
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
// Open personal cabinet (placeholder)
|
||||
function openPersonal() {
|
||||
alert('Личный кабинет в разработке 🔜\n\nВы можете войти через:\n• Мобильное приложение\n• my.kazakhtelecom.kz');
|
||||
}
|
||||
|
||||
290
data.json
290
data.json
@ -1,8 +1,286 @@
|
||||
{
|
||||
"users": [],
|
||||
"chats": [],
|
||||
"messages": [],
|
||||
"groups": [],
|
||||
"channels": [],
|
||||
"sessions": []
|
||||
"siteSettings": {
|
||||
"heroTitle": "Kazakhtelecom",
|
||||
"heroSubtitle": "Цифровое будущее Казахстана",
|
||||
"heroDescription": "Высокоскоростной интернет, цифровое ТВ и инновационные технологии для вашего дома и бизнеса",
|
||||
"primaryColor": "#0066CC",
|
||||
"accentColor": "#00C9FF"
|
||||
},
|
||||
|
||||
"services": [
|
||||
{
|
||||
"id": "internet",
|
||||
"name": "Домашний интернет",
|
||||
"icon": "🌐",
|
||||
"description": "Высокоскоростной оптоволоконный интернет",
|
||||
"features": ["Скорость до 1 Гбит/с", "Безлимитный трафик", "Стабильное соединение", "Поддержка 24/7"],
|
||||
"color": "#0066CC"
|
||||
},
|
||||
{
|
||||
"id": "tv",
|
||||
"name": "Цифровое TV",
|
||||
"icon": "📺",
|
||||
"description": "Более 200 каналов в высоком качестве",
|
||||
"features": ["4K Ultra HD", "Интерактивное ТВ", "Архив передач", "Родительский контроль"],
|
||||
"color": "#7B2CBF"
|
||||
},
|
||||
{
|
||||
"id": "mobile",
|
||||
"name": "Мобильная связь",
|
||||
"icon": "📱",
|
||||
"description": "Современные тарифы для всей семьи",
|
||||
"features": ["Безлимитные звонки", "5G интернет", "Семейные тарифы", "Роуминг по миру"],
|
||||
"color": "#FF006E"
|
||||
},
|
||||
{
|
||||
"id": "smart",
|
||||
"name": "Умный дом",
|
||||
"icon": "🏠",
|
||||
"description": "Автоматизация и безопасность вашего дома",
|
||||
"features": ["Видеонаблюдение", "Умные датчики", "Управление со смартфона", "Система безопасности"],
|
||||
"color": "#00B4D8"
|
||||
},
|
||||
{
|
||||
"id": "cloud",
|
||||
"name": "Облачные сервисы",
|
||||
"icon": "☁️",
|
||||
"description": "Хранение данных и онлайн-сервисы",
|
||||
"features": ["Облачное хранилище", "Онлайн-офис", "Резервное копирование", "Доступ отовсюду"],
|
||||
"color": "#3A86FF"
|
||||
},
|
||||
{
|
||||
"id": "business",
|
||||
"name": "Бизнесу",
|
||||
"icon": "💼",
|
||||
"description": "Решения для корпоративных клиентов",
|
||||
"features": ["Выделенные линии", "IP-телефония", "ЦОД услуги", "Техническая поддержка"],
|
||||
"color": "#8338EC"
|
||||
}
|
||||
],
|
||||
|
||||
"tariffs": {
|
||||
"internet": [
|
||||
{
|
||||
"id": "net-start",
|
||||
"name": "Старт",
|
||||
"speed": "100 Мбит/с",
|
||||
"price": 3990,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["Безлимитный трафик", "Wi-Fi роутер в аренду", "Антивирус в подарок", "Техподдержка 24/7"]
|
||||
},
|
||||
{
|
||||
"id": "net-optimal",
|
||||
"name": "Оптимальный",
|
||||
"speed": "300 Мбит/с",
|
||||
"price": 5990,
|
||||
"currency": "₸",
|
||||
"popular": true,
|
||||
"features": ["Безлимитный трафик", "Двухдиапазонный роутер", "Антивирус + Родительский контроль", "Приоритетная поддержка"]
|
||||
},
|
||||
{
|
||||
"id": "net-max",
|
||||
"name": "Максимальный",
|
||||
"speed": "1 Гбит/с",
|
||||
"price": 8990,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["Гигабитный доступ", "Mesh-система", "Премиум защита", "Персональный менеджер"]
|
||||
},
|
||||
{
|
||||
"id": "net-gamer",
|
||||
"name": "Геймер",
|
||||
"speed": "500 Мбит/с",
|
||||
"price": 7490,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["Низкий пинг", "Игровой приоритет", "Защита от DDoS", "Бонусы от партнеров"]
|
||||
}
|
||||
],
|
||||
|
||||
"tv": [
|
||||
{
|
||||
"id": "tv-light",
|
||||
"name": "Лёгкий",
|
||||
"channels": "100+ каналов",
|
||||
"price": 2990,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["HD качество", "Архив 3 дня", "Базовые пакеты", "Пульт в комплекте"]
|
||||
},
|
||||
{
|
||||
"id": "tv-standard",
|
||||
"name": "Стандарт",
|
||||
"channels": "150+ каналов",
|
||||
"price": 4490,
|
||||
"currency": "₸",
|
||||
"popular": true,
|
||||
"features": ["Full HD", "Архив 7 дней", "Премьеры и сериалы", "4K контент"]
|
||||
},
|
||||
{
|
||||
"id": "tv-premium",
|
||||
"name": "Премиум",
|
||||
"channels": "200+ каналов",
|
||||
"price": 6990,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["4K Ultra HD", "Архив 14 дней", "Все премиум пакеты", "Онлайн-кинотеатр"]
|
||||
}
|
||||
],
|
||||
|
||||
"mobile": [
|
||||
{
|
||||
"id": "mobile-light",
|
||||
"name": "Лёгкий",
|
||||
"price": 2500,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["Безлимитные звонки по КЗ", "10 ГБ интернета", "SMS безлимит", "Соцсети без трафика"]
|
||||
},
|
||||
{
|
||||
"id": "mobile-standard",
|
||||
"name": "Стандарт",
|
||||
"price": 4500,
|
||||
"currency": "₸",
|
||||
"popular": true,
|
||||
"features": ["Безлимитные звонки", "30 ГБ интернета", "Безлимит на мессенджеры", "5G доступ"]
|
||||
},
|
||||
{
|
||||
"id": "mobile-unlimited",
|
||||
"name": "Безлимит",
|
||||
"price": 7500,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["Всё безлимитно", "50 ГБ на высокой скорости", "Роуминг СНГ включён", "Приоритет в сети"]
|
||||
},
|
||||
{
|
||||
"id": "mobile-family",
|
||||
"name": "Семейный",
|
||||
"price": 12000,
|
||||
"currency": "₸",
|
||||
"popular": false,
|
||||
"features": ["4 сим-карты", "Общий пакет 100 ГБ", "Безлимит внутри семьи", "Детский контроль"]
|
||||
}
|
||||
],
|
||||
|
||||
"smart": [
|
||||
{
|
||||
"id": "smart-basic",
|
||||
"name": "Базовый",
|
||||
"price": 4990,
|
||||
"currency": "₸/мес",
|
||||
"popular": false,
|
||||
"features": ["2 умные лампочки", "1 датчик движения", "Управление со смартфона", "Базовые сценарии"]
|
||||
},
|
||||
{
|
||||
"id": "smart-comfort",
|
||||
"name": "Комфорт",
|
||||
"price": 8990,
|
||||
"currency": "₸/мес",
|
||||
"popular": true,
|
||||
"features": ["5 умных устройств", "Видеодомофон", "Датчики протечки", "Голосовое управление"]
|
||||
},
|
||||
{
|
||||
"id": "smart-security",
|
||||
"name": "Безопасность",
|
||||
"price": 12990,
|
||||
"currency": "₸/мес",
|
||||
"popular": false,
|
||||
"features": ["Видеонаблюдение 4 камеры", "Датчики открытия", "Сигнализация", "Мониторинг 24/7"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"bundles": [
|
||||
{
|
||||
"id": "bundle-family",
|
||||
"name": "Семейный",
|
||||
"description": "Интернет + ТВ + Мобильная связь для всей семьи",
|
||||
"originalPrice": 15470,
|
||||
"bundlePrice": 11990,
|
||||
"discount": 23,
|
||||
"includes": ["Интернет 300 Мбит/с", "Цифровое ТВ 150+ каналов", "2 мобильных тарифа Стандарт"],
|
||||
"popular": true
|
||||
},
|
||||
{
|
||||
"id": "bundle-max",
|
||||
"name": "Максимальный",
|
||||
"description": "Все услуги в одном пакете",
|
||||
"originalPrice": 22970,
|
||||
"bundlePrice": 17990,
|
||||
"discount": 22,
|
||||
"includes": ["Интернет 1 Гбит/с", "Цифровое ТВ Премиум", "Мобильный тариф Безлимит", "Умный дом Комфорт"],
|
||||
"popular": false
|
||||
},
|
||||
{
|
||||
"id": "bundle-gamer",
|
||||
"name": "Геймер",
|
||||
"description": "Для настоящих геймеров и стримеров",
|
||||
"originalPrice": 14480,
|
||||
"bundlePrice": 11490,
|
||||
"discount": 21,
|
||||
"includes": ["Интернет Геймер 500 Мбит/с", "ТВ Стандарт", "Мобильный тариф Стандарт", "Игровые бонусы"],
|
||||
"popular": false
|
||||
}
|
||||
],
|
||||
|
||||
"news": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Запущен 5G в Алматы",
|
||||
"date": "2026-08-15",
|
||||
"category": "technology",
|
||||
"image": "https://picsum.photos/600/400?random=1",
|
||||
"excerpt": "Теперь абоненты могут пользоваться сверхбыстрым интернетом пятого поколения"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Новые тарифы на домашний интернет",
|
||||
"date": "2026-08-10",
|
||||
"category": "tariffs",
|
||||
"image": "https://picsum.photos/600/400?random=2",
|
||||
"excerpt": "Увеличили скорости без изменения стоимости для всех тарифов"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Умный дом стал доступнее",
|
||||
"date": "2026-08-05",
|
||||
"category": "products",
|
||||
"image": "https://picsum.photos/600/400?random=3",
|
||||
"excerpt": "Скидки до 30% на оборудование для системы умный дом"
|
||||
}
|
||||
],
|
||||
|
||||
"contacts": {
|
||||
"phone": "123",
|
||||
"phoneFormatted": "123 (бесплатно с городских)",
|
||||
"mobile": "+7 (777) 123-45-67",
|
||||
"email": "info@kazakhtelecom.kz",
|
||||
"address": "г. Алматы, ул. Байтурсынова, 112Б",
|
||||
"social": {
|
||||
"instagram": "https://instagram.com/kazakhtelecom",
|
||||
"facebook": "https://facebook.com/kazakhtelecom",
|
||||
"telegram": "https://t.me/kazakhtelecom",
|
||||
"youtube": "https://youtube.com/kazakhtelecom"
|
||||
}
|
||||
},
|
||||
|
||||
"faq": [
|
||||
{
|
||||
"question": "Как подключить домашний интернет?",
|
||||
"answer": "Оставьте заявку на сайте или позвоните по номеру 123. Наш специалист свяжется с вами в течение 15 минут для уточнения деталей и назначения времени подключения."
|
||||
},
|
||||
{
|
||||
"question": "Какая скорость интернета мне подойдет?",
|
||||
"answer": "Для просмотра сайтов и соцсетей достаточно 100 Мбит/с. Для стриминга 4K и онлайн-игр рекомендуем от 300 Мбит/с. Для умного дома и множества устройств — 1 Гбит/с."
|
||||
},
|
||||
{
|
||||
"question": "Можно ли заморозить услуги на время отъезда?",
|
||||
"answer": "Да, вы можете приостановить действие тарифа на срок от 14 до 90 дней через личный кабинет или по звонку в контактный центр."
|
||||
},
|
||||
{
|
||||
"question": "Предоставляете ли вы оборудование в аренду?",
|
||||
"answer": "Да, мы предоставляем современные Wi-Fi роутеры и TV-приставки в аренду за символическую плату. Также возможна покупка оборудования в рассрочку."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
332
index.html
332
index.html
@ -3,136 +3,224 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VibeChat — Мессенджер будущего</title>
|
||||
<link rel="stylesheet" href="landing.css">
|
||||
<title>Kazakhtelecom — Цифровое будущее Казахстана</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Landing Page -->
|
||||
<div id="landingPage">
|
||||
<nav class="landing-nav">
|
||||
<div class="nav-container">
|
||||
<div class="logo">💬 VibeChat</div>
|
||||
<button class="btn-login" onclick="showAuth('login')">Войти</button>
|
||||
<!-- Navigation -->
|
||||
<nav class="nav" id="nav">
|
||||
<div class="nav-container">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">📡</span>
|
||||
<span class="logo-text">Kazakhtelecom</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="nav-links">
|
||||
<a href="#services">Услуги</a>
|
||||
<a href="#tariffs">Тарифы</a>
|
||||
<a href="#bundles">Пакеты</a>
|
||||
<a href="#news">Новости</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
<a href="#contacts">Контакты</a>
|
||||
</div>
|
||||
<button class="btn-personal" onclick="openPersonal()">Личный кабинет</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h1>Общение без границ</h1>
|
||||
<p>Быстрый. Безопасный. С End-to-End шифрованием.</p>
|
||||
<!-- Hero Section -->
|
||||
<section class="hero" id="hero">
|
||||
<div class="hero-bg">
|
||||
<div class="gradient-orb orb-1"></div>
|
||||
<div class="gradient-orb orb-2"></div>
|
||||
<div class="gradient-orb orb-3"></div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="glass-card hero-card">
|
||||
<h1 id="heroTitle"></h1>
|
||||
<p id="heroSubtitle" class="hero-subtitle"></p>
|
||||
<p id="heroDescription" class="hero-description"></p>
|
||||
<div class="hero-buttons">
|
||||
<button class="btn-primary" onclick="showAuth('register')">Начать бесплатно</button>
|
||||
<button class="btn-secondary" onclick="scrollToFeatures()">Узнать больше</button>
|
||||
<button class="btn-primary" onclick="scrollToSection('tariffs')">Выбрать тариф</button>
|
||||
<button class="btn-secondary" onclick="scrollToSection('services')">Узнать больше</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-image">
|
||||
<div class="chat-mockup">
|
||||
<div class="mockup-header">
|
||||
<div class="mockup-avatar"></div>
|
||||
<div class="mockup-lines">
|
||||
<div class="line short"></div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup-messages">
|
||||
<div class="message received"></div>
|
||||
<div class="message sent"></div>
|
||||
<div class="message received"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features" id="features">
|
||||
<h2>Почему VibeChat?</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h3>E2E Шифрование</h3>
|
||||
<p>Ваши сообщения видят только отправитель и получатель</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>Молниеносно</h3>
|
||||
<p>Мгновенная доставка сообщений в любую точку мира</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h3>Группы и каналы</h3>
|
||||
<p>Создавайте группы до 1000 человек и публичные каналы</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>Эмодзи и стикеры</h3>
|
||||
<p>Тысячи эмодзи и стикеров для выражения эмоций</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📱</div>
|
||||
<h3>Кроссплатформенно</h3>
|
||||
<p>Работает на любом устройстве через браузер</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔐</div>
|
||||
<h3>Безопасность</h3>
|
||||
<p>Двухфакторная аутентификация и секретные чаты</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta">
|
||||
<h2>Готовы начать?</h2>
|
||||
<p>Присоединяйтесь к тысячам пользователей уже сегодня</p>
|
||||
<button class="btn-primary" onclick="showAuth('register')">Создать аккаунт</button>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>© 2026 VibeChat. Все права защищены.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Auth Modal -->
|
||||
<div id="authModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close" onclick="closeAuth()">×</button>
|
||||
|
||||
<div id="loginForm">
|
||||
<h2>Вход</h2>
|
||||
<form onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="loginEmail" required placeholder="you@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Пароль</label>
|
||||
<input type="password" id="loginPassword" required placeholder="••••••••">
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Войти</button>
|
||||
</form>
|
||||
<p class="auth-switch">Нет аккаунта? <a href="#" onclick="showAuth('register')">Зарегистрироваться</a></p>
|
||||
</div>
|
||||
|
||||
<div id="registerForm" style="display:none">
|
||||
<h2>Регистрация</h2>
|
||||
<form onsubmit="handleRegister(event)">
|
||||
<div class="form-group">
|
||||
<label>Имя пользователя</label>
|
||||
<input type="text" id="regUsername" required placeholder="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="regEmail" required placeholder="you@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Пароль</label>
|
||||
<input type="password" id="regPassword" required placeholder="••••••••" minlength="6">
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Создать аккаунт</button>
|
||||
</form>
|
||||
<p class="auth-switch">Уже есть аккаунт? <a href="#" onclick="showAuth('login')">Войти</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-scroll">
|
||||
<div class="mouse">
|
||||
<div class="wheel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="landing.js"></script>
|
||||
<!-- Services -->
|
||||
<section class="services" id="services">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Наши услуги</h2>
|
||||
<p class="section-subtitle">Полный спектр цифровых услуг для вашего комфорта</p>
|
||||
</div>
|
||||
<div class="services-grid" id="servicesGrid">
|
||||
<!-- Services rendered by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tariffs -->
|
||||
<section class="tariffs" id="tariffs">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Тарифные планы</h2>
|
||||
<p class="section-subtitle">Выберите подходящий тариф</p>
|
||||
<div class="tariff-tabs" id="tariffTabs">
|
||||
<button class="tab active" data-category="internet">Интернет</button>
|
||||
<button class="tab" data-category="tv">ТВ</button>
|
||||
<button class="tab" data-category="mobile">Мобильная связь</button>
|
||||
<button class="tab" data-category="smart">Умный дом</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tariffs-grid" id="tariffsGrid">
|
||||
<!-- Tariffs rendered by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bundles -->
|
||||
<section class="bundles" id="bundles">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Выгодные пакеты</h2>
|
||||
<p class="section-subtitle">Экономьте до 30% при подключении нескольких услуг</p>
|
||||
</div>
|
||||
<div class="bundles-grid" id="bundlesGrid">
|
||||
<!-- Bundles rendered by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats -->
|
||||
<section class="stats">
|
||||
<div class="stats-container">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" data-target="5000000">0</div>
|
||||
<div class="stat-label">Абонентов по всему Казахстану</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" data-target="25">0</div>
|
||||
<div class="stat-label">Лет на рынке</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" data-target="100">0</div>
|
||||
<div class="stat-label">Городов покрытия</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" data-target="24">0</div>
|
||||
<div class="stat-label">Поддержка 24/7</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- News -->
|
||||
<section class="news" id="news">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Новости и события</h2>
|
||||
<p class="section-subtitle">Будьте в курсе последних событий</p>
|
||||
</div>
|
||||
<div class="news-grid" id="newsGrid">
|
||||
<!-- News rendered by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section class="faq" id="faq">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Частые вопросы</h2>
|
||||
<p class="section-subtitle">Ответы на популярные вопросы</p>
|
||||
</div>
|
||||
<div class="faq-container" id="faqContainer">
|
||||
<!-- FAQ rendered by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<section class="contact-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Оставить заявку</h2>
|
||||
<p class="section-subtitle">Мы перезвоним вам в течение 15 минут</p>
|
||||
</div>
|
||||
<div class="contact-form-container">
|
||||
<form class="contact-form" id="contactForm" onsubmit="submitContact(event)">
|
||||
<div class="form-group">
|
||||
<label>Ваше имя</label>
|
||||
<input type="text" id="contactName" required placeholder="Иван Иванов">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Телефон</label>
|
||||
<input type="tel" id="contactPhone" required placeholder="+7 (___) ___-__-__">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Интересующая услуга</label>
|
||||
<select id="contactService">
|
||||
<option value="">Выберите услугу</option>
|
||||
<option value="internet">Домашний интернет</option>
|
||||
<option value="tv">Цифровое ТВ</option>
|
||||
<option value="mobile">Мобильная связь</option>
|
||||
<option value="smart">Умный дом</option>
|
||||
<option value="bundle">Пакетное подключение</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Отправить заявку</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer" id="contacts">
|
||||
<div class="footer-container">
|
||||
<div class="footer-column">
|
||||
<div class="footer-logo">
|
||||
<span class="logo-icon">📡</span>
|
||||
<span class="logo-text">Kazakhtelecom</span>
|
||||
</div>
|
||||
<p class="footer-description">Цифровое будущее Казахстана</p>
|
||||
<div class="social-links">
|
||||
<a href="#" class="social-link">Instagram</a>
|
||||
<a href="#" class="social-link">Facebook</a>
|
||||
<a href="#" class="social-link">Telegram</a>
|
||||
<a href="#" class="social-link">YouTube</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-column">
|
||||
<h4>Услуги</h4>
|
||||
<a href="#tariffs">Домашний интернет</a>
|
||||
<a href="#tariffs">Цифровое ТВ</a>
|
||||
<a href="#tariffs">Мобильная связь</a>
|
||||
<a href="#tariffs">Умный дом</a>
|
||||
</div>
|
||||
<div class="footer-column">
|
||||
<h4>Компания</h4>
|
||||
<a href="#">О нас</a>
|
||||
<a href="#news">Новости</a>
|
||||
<a href="#">Карьера</a>
|
||||
<a href="#faq">Помощь</a>
|
||||
</div>
|
||||
<div class="footer-column">
|
||||
<h4>Контакты</h4>
|
||||
<div class="contact-info">
|
||||
<span class="contact-label">Телефон:</span>
|
||||
<a href="tel:123" class="contact-value">123</a>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<span class="contact-label">Мобильный:</span>
|
||||
<a href="tel:+77771234567" class="contact-value">+7 (777) 123-45-67</a>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<span class="contact-label">Email:</span>
|
||||
<a href="mailto:info@kazakhtelecom.kz" class="contact-value">info@kazakhtelecom.kz</a>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<span class="contact-label">Адрес:</span>
|
||||
<span class="contact-value">г. Алматы, ул. Байтурсынова, 112Б</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Kazakhtelecom. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "apple-style-app",
|
||||
"name": "vibechat-messenger",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "apple-style-app"
|
||||
"name": "vibechat-messenger"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "vibechat-messenger",
|
||||
"name": "kazakhtelecom-website",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
362
server.js
362
server.js
@ -1,18 +1,12 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA = path.join(__dirname, "data.json");
|
||||
const SECRET_KEY = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
function readData() {
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { users: [], chats: [], messages: [], groups: [], channels: [], sessions: [] }; }
|
||||
}
|
||||
|
||||
function writeData(data) {
|
||||
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return {}; }
|
||||
}
|
||||
|
||||
function parseBody(req) {
|
||||
@ -28,326 +22,18 @@ function json(res, data, status = 200) {
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
function notFound(res) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
}
|
||||
|
||||
// Генерация токена сессии
|
||||
function generateToken(userId) {
|
||||
return crypto.createHmac("sha256", SECRET_KEY).update(userId + Date.now()).digest("hex");
|
||||
}
|
||||
|
||||
// Проверка токена
|
||||
function verifyToken(token) {
|
||||
const data = readData();
|
||||
const session = data.sessions.find(s => s.token === token && new Date(s.expires) > new Date());
|
||||
if (!session) return null;
|
||||
return data.users.find(u => u.id === session.userId);
|
||||
}
|
||||
|
||||
// Хеширование пароля
|
||||
function hashPassword(password) {
|
||||
return crypto.createHash("sha256").update(password + "vibechat_salt").digest("hex");
|
||||
}
|
||||
|
||||
// E2E шифрование (упрощённое AES-GCM)
|
||||
function encryptMessage(text, key) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", Buffer.from(key.slice(0, 32).padEnd(32, "0")), iv);
|
||||
let encrypted = cipher.update(text, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag().toString("hex");
|
||||
return { iv: iv.toString("hex"), encrypted, authTag };
|
||||
}
|
||||
|
||||
function decryptMessage(encryptedData, key) {
|
||||
try {
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(key.slice(0, 32).padEnd(32, "0")), Buffer.from(encryptedData.iv, "hex"));
|
||||
decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex"));
|
||||
let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
return "[Ошибка расшифровки]";
|
||||
}
|
||||
}
|
||||
|
||||
const routes = {
|
||||
// ========== AUTH ==========
|
||||
"POST /api/auth/register": async (req, res) => {
|
||||
const { username, email, password } = await parseBody(req);
|
||||
if (!username || !email || !password) return json(res, { ok: false, error: "Все поля обязательны" }, 400);
|
||||
|
||||
"GET /api/data": (req, res) => {
|
||||
const data = readData();
|
||||
if (data.users.find(u => u.email === email)) return json(res, { ok: false, error: "Email уже зарегистрирован" }, 400);
|
||||
if (data.users.find(u => u.username === username)) return json(res, { ok: false, error: "Имя пользователя занято" }, 400);
|
||||
|
||||
const user = {
|
||||
id: Date.now(),
|
||||
username,
|
||||
email,
|
||||
passwordHash: hashPassword(password),
|
||||
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`,
|
||||
bio: "",
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
data.users.push(user);
|
||||
writeData(data);
|
||||
|
||||
const token = generateToken(user.id);
|
||||
data.sessions.push({ token, userId: user.id, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() });
|
||||
writeData(data);
|
||||
|
||||
json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } });
|
||||
json(res, { ok: true, data });
|
||||
},
|
||||
|
||||
"POST /api/auth/login": async (req, res) => {
|
||||
const { email, password } = await parseBody(req);
|
||||
if (!email || !password) return json(res, { ok: false, error: "Email и пароль обязательны" }, 400);
|
||||
|
||||
const data = readData();
|
||||
const user = data.users.find(u => u.email === email && u.passwordHash === hashPassword(password));
|
||||
if (!user) return json(res, { ok: false, error: "Неверный email или пароль" }, 401);
|
||||
|
||||
const token = generateToken(user.id);
|
||||
data.sessions.push({ token, userId: user.id, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() });
|
||||
writeData(data);
|
||||
|
||||
json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } });
|
||||
},
|
||||
|
||||
"POST /api/auth/logout": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Токен не предоставлен" }, 401);
|
||||
|
||||
const data = readData();
|
||||
data.sessions = data.sessions.filter(s => s.token !== token);
|
||||
writeData(data);
|
||||
json(res, { ok: true });
|
||||
},
|
||||
|
||||
"GET /api/auth/me": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
|
||||
const user = verifyToken(token);
|
||||
if (!user) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
json(res, { ok: true, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar, bio: user.bio } });
|
||||
},
|
||||
|
||||
// ========== USERS ==========
|
||||
"GET /api/users": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const data = readData();
|
||||
const users = data.users.filter(u => u.id !== currentUser.id).map(u => ({
|
||||
id: u.id, username: u.username, avatar: u.avatar
|
||||
}));
|
||||
json(res, { ok: true, users });
|
||||
},
|
||||
|
||||
"PUT /api/users/me": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const { username, bio, avatar } = await parseBody(req);
|
||||
const data = readData();
|
||||
const idx = data.users.findIndex(u => u.id === currentUser.id);
|
||||
if (idx === -1) return json(res, { ok: false, error: "Пользователь не найден" }, 404);
|
||||
|
||||
if (username) data.users[idx].username = username;
|
||||
if (bio !== undefined) data.users[idx].bio = bio;
|
||||
if (avatar) data.users[idx].avatar = avatar;
|
||||
writeData(data);
|
||||
|
||||
json(res, { ok: true, user: data.users[idx] });
|
||||
},
|
||||
|
||||
// ========== CHATS ==========
|
||||
"GET /api/chats": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const data = readData();
|
||||
const userChats = data.chats.filter(c => c.participants.includes(currentUser.id));
|
||||
"POST /api/contact": async (req, res) => {
|
||||
const { name, phone, service } = await parseBody(req);
|
||||
if (!name || !phone) return json(res, { ok: false, error: "Имя и телефон обязательны" }, 400);
|
||||
|
||||
const chatsWithDetails = userChats.map(chat => {
|
||||
const otherUserId = chat.type === "private" ? chat.participants.find(p => p !== currentUser.id) : null;
|
||||
const otherUser = otherUserId ? data.users.find(u => u.id === otherUserId) : null;
|
||||
const lastMessage = data.messages.filter(m => m.chatId === chat.id).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0];
|
||||
|
||||
return {
|
||||
...chat,
|
||||
name: chat.type === "private" ? otherUser?.username : chat.name,
|
||||
avatar: chat.type === "private" ? otherUser?.avatar : chat.avatar,
|
||||
lastMessage: lastMessage ? { text: lastMessage.text, createdAt: lastMessage.createdAt } : null
|
||||
};
|
||||
});
|
||||
|
||||
json(res, { ok: true, chats: chatsWithDetails });
|
||||
},
|
||||
|
||||
"POST /api/chats": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const { type, participantId, name } = await parseBody(req);
|
||||
const data = readData();
|
||||
|
||||
if (type === "private") {
|
||||
const existingChat = data.chats.find(c => c.type === "private" && c.participants.includes(currentUser.id) && c.participants.includes(participantId));
|
||||
if (existingChat) return json(res, { ok: true, chat: existingChat });
|
||||
|
||||
const chat = {
|
||||
id: Date.now(),
|
||||
type: "private",
|
||||
participants: [currentUser.id, participantId],
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.chats.push(chat);
|
||||
writeData(data);
|
||||
json(res, { ok: true, chat });
|
||||
} else if (type === "group") {
|
||||
const chat = {
|
||||
id: Date.now(),
|
||||
type: "group",
|
||||
name: name || "Группа",
|
||||
participants: [currentUser.id],
|
||||
avatar: `https://api.dicebear.com/7.x/identicon/svg?seed=${Date.now()}`,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.chats.push(chat);
|
||||
writeData(data);
|
||||
json(res, { ok: true, chat });
|
||||
} else if (type === "channel") {
|
||||
const channel = {
|
||||
id: Date.now(),
|
||||
type: "channel",
|
||||
name: name || "Канал",
|
||||
ownerId: currentUser.id,
|
||||
subscribers: [currentUser.id],
|
||||
avatar: `https://api.dicebear.com/7.x/identicon/svg?seed=${Date.now()}`,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.channels.push(channel);
|
||||
data.chats.push({ id: channel.id, type: "channel", name: channel.name, ownerId: channel.id, subscribers: [currentUser.id], avatar: channel.avatar, createdAt: channel.createdAt });
|
||||
writeData(data);
|
||||
json(res, { ok: true, chat: channel });
|
||||
}
|
||||
},
|
||||
|
||||
"GET /api/chats/:id/messages": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const chatId = parseInt(params.id);
|
||||
const data = readData();
|
||||
const chat = data.chats.find(c => c.id === chatId);
|
||||
if (!chat || !chat.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Чат не найден" }, 404);
|
||||
|
||||
const messages = data.messages.filter(m => m.chatId === chatId).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||
|
||||
// E2E расшифровка
|
||||
const userKey = hashPassword(currentUser.email + currentUser.id);
|
||||
const decryptedMessages = messages.map(m => ({
|
||||
...m,
|
||||
text: decryptMessage(m.encrypted, userKey)
|
||||
}));
|
||||
|
||||
json(res, { ok: true, messages: decryptedMessages });
|
||||
},
|
||||
|
||||
"POST /api/chats/:id/messages": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const chatId = parseInt(params.id);
|
||||
const { text, type = "text" } = await parseBody(req);
|
||||
const data = readData();
|
||||
|
||||
const chat = data.chats.find(c => c.id === chatId);
|
||||
if (!chat || !chat.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Чат не найден" }, 404);
|
||||
|
||||
// E2E шифрование
|
||||
const userKey = hashPassword(currentUser.email + currentUser.id);
|
||||
const encrypted = encryptMessage(text, userKey);
|
||||
|
||||
const message = {
|
||||
id: Date.now(),
|
||||
chatId,
|
||||
senderId: currentUser.id,
|
||||
senderName: currentUser.username,
|
||||
senderAvatar: currentUser.avatar,
|
||||
text,
|
||||
encrypted,
|
||||
type,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
data.messages.push(message);
|
||||
writeData(data);
|
||||
json(res, { ok: true, message: { ...message, text } });
|
||||
},
|
||||
|
||||
// ========== GROUPS ==========
|
||||
"POST /api/groups/:id/members": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const groupId = parseInt(params.id);
|
||||
const { userId } = await parseBody(req);
|
||||
const data = readData();
|
||||
|
||||
const group = data.chats.find(c => c.id === groupId && c.type === "group");
|
||||
if (!group || !group.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Группа не найдена" }, 404);
|
||||
|
||||
if (!group.participants.includes(userId)) {
|
||||
group.participants.push(userId);
|
||||
writeData(data);
|
||||
}
|
||||
json(res, { ok: true, group });
|
||||
},
|
||||
|
||||
// ========== CHANNELS ==========
|
||||
"POST /api/channels/:id/subscribe": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const channelId = parseInt(params.id);
|
||||
const data = readData();
|
||||
|
||||
const channel = data.channels.find(c => c.id === channelId);
|
||||
if (!channel) return json(res, { ok: false, error: "Канал не найден" }, 404);
|
||||
|
||||
if (!channel.subscribers.includes(currentUser.id)) {
|
||||
channel.subscribers.push(currentUser.id);
|
||||
const chatIdx = data.chats.findIndex(c => c.id === channelId);
|
||||
if (chatIdx !== -1 && !data.chats[chatIdx].subscribers.includes(currentUser.id)) {
|
||||
data.chats[chatIdx].subscribers.push(currentUser.id);
|
||||
}
|
||||
writeData(data);
|
||||
}
|
||||
json(res, { ok: true, channel });
|
||||
console.log(`📞 Заявка: ${name}, ${phone}, ${service || 'Не указано'}`);
|
||||
json(res, { ok: true, message: "Заявка отправлена! Мы свяжемся с вами в течение 15 минут." });
|
||||
}
|
||||
};
|
||||
|
||||
@ -363,41 +49,20 @@ http.createServer(async (req, res) => {
|
||||
const method = req.method;
|
||||
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (method === "OPTIONS") {
|
||||
res.writeHead(204);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
// API ROUTING
|
||||
for (const [route, handler] of Object.entries(routes)) {
|
||||
const [m, p] = route.split(" ");
|
||||
if (method !== m) continue;
|
||||
|
||||
const routeParts = p.split("/");
|
||||
const urlParts = url.split("/");
|
||||
|
||||
if (routeParts.length !== urlParts.length) continue;
|
||||
|
||||
const params = {};
|
||||
let match = true;
|
||||
for (let i = 0; i < routeParts.length; i++) {
|
||||
if (routeParts[i].startsWith(":")) {
|
||||
params[routeParts[i].slice(1)] = urlParts[i];
|
||||
} else if (routeParts[i] !== urlParts[i]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
return handler(req, res, params);
|
||||
}
|
||||
if (p === url) return handler(req, res);
|
||||
}
|
||||
|
||||
// STATIC FILES
|
||||
let file = url === "/" ? "/index.html" : url;
|
||||
const full = path.join(__dirname, file);
|
||||
|
||||
@ -408,5 +73,6 @@ http.createServer(async (req, res) => {
|
||||
return res.end(fs.readFileSync(full));
|
||||
}
|
||||
|
||||
notFound(res);
|
||||
}).listen(PORT, () => console.log("VibeChat server running on port " + PORT));
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
}).listen(PORT, () => console.log("Kazakhtelecom site running on port " + PORT));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user