Kazakhtelecom: Modern Liquid Glass website with all services, tariffs, bundles

This commit is contained in:
temp 2026-08-17 11:38:39 +00:00
parent e5deccd85e
commit e09fb66b25
7 changed files with 1669 additions and 1344 deletions

689
app.js
View File

@ -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
View File

@ -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-приставки в аренду за символическую плату. Также возможна покупка оборудования в рассрочку."
}
]
}

View File

@ -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>&copy; 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>&copy; 2026 Kazakhtelecom. Все права защищены.</p>
</div>
</footer>
<script src="app.js"></script>
</body>
</html>

4
package-lock.json generated
View File

@ -1,10 +1,10 @@
{
"name": "apple-style-app",
"name": "vibechat-messenger",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "apple-style-app"
"name": "vibechat-messenger"
}
}
}

View File

@ -1,9 +1,8 @@
{
"name": "vibechat-messenger",
"name": "kazakhtelecom-website",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node server.js"
},
"dependencies": {}
}
}

362
server.js
View File

@ -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));

1331
style.css

File diff suppressed because it is too large Load Diff