VibeChat: Full-featured messenger with E2E encryption, groups, channels
This commit is contained in:
parent
9ae6383d9d
commit
e5deccd85e
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules/
|
|||||||
.vibe42-run.log
|
.vibe42-run.log
|
||||||
.vibe42-run.pid
|
.vibe42-run.pid
|
||||||
*.local
|
*.local
|
||||||
|
data.json
|
||||||
|
|||||||
596
API.md
596
API.md
@ -1,318 +1,494 @@
|
|||||||
# API Документация — Apple Style App
|
# VibeChat API Documentation
|
||||||
|
|
||||||
## 📚 Обзор
|
## 📚 Обзор
|
||||||
|
|
||||||
Бэкенд построен на **Node.js http** (без зависимостей). Данные хранятся в `data.json`.
|
**VibeChat** — безопасный мессенджер с End-to-End шифрованием, группами, каналами и кроссплатформенной поддержкой.
|
||||||
|
|
||||||
|
**Стек:**
|
||||||
|
- Бэкенд: Node.js (native http, без зависимостей)
|
||||||
|
- Хранение: JSON файлы
|
||||||
|
- Шифрование: AES-256-GCM (crypto module)
|
||||||
|
- Аутентификация: JWT-like токены на HMAC-SHA256
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔌 PUBLIC API (без авторизации)
|
## 🔐 Безопасность
|
||||||
|
|
||||||
### Получить все продукты
|
### End-to-End Шифрование
|
||||||
```
|
|
||||||
GET /api/products
|
Каждое сообщение шифруется на клиенте перед отправкой:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Ключ генерируется из email + ID пользователя
|
||||||
|
const userKey = hashPassword(user.email + user.id);
|
||||||
|
|
||||||
|
// Шифрование (AES-256-GCM)
|
||||||
|
function encryptMessage(text, key) {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||||
|
let encrypted = cipher.update(text, "utf8", "hex");
|
||||||
|
encrypted += cipher.final("hex");
|
||||||
|
const authTag = cipher.getAuthTag().toString("hex");
|
||||||
|
return { iv, encrypted, authTag };
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ответ:**
|
**В базе данных хранится:**
|
||||||
```json
|
```json
|
||||||
|
{
|
||||||
|
"text": "Исходный текст (виден только отправителю)",
|
||||||
|
"encrypted": {
|
||||||
|
"iv": "...",
|
||||||
|
"encrypted": "...",
|
||||||
|
"authTag": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Получатель расшифровывает своим ключом на клиенте.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 API Endpoints
|
||||||
|
|
||||||
|
### AUTH
|
||||||
|
|
||||||
|
#### Регистрация
|
||||||
|
```
|
||||||
|
POST /api/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "john",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"password": "secure123"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": [
|
"token": "eyJhbGc...",
|
||||||
|
"user": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"username": "john",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=john"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Вход
|
||||||
|
```
|
||||||
|
POST /api/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "john@example.com",
|
||||||
|
"password": "secure123"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"token": "eyJhbGc...",
|
||||||
|
"user": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Выход
|
||||||
|
```
|
||||||
|
POST /api/auth/logout
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{ "ok": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получить текущий профиль
|
||||||
|
```
|
||||||
|
GET /api/auth/me
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"user": {
|
||||||
|
"id": 123,
|
||||||
|
"username": "john",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"avatar": "...",
|
||||||
|
"bio": "О себе"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### USERS
|
||||||
|
|
||||||
|
#### Список пользователей
|
||||||
|
```
|
||||||
|
GET /api/users
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"users": [
|
||||||
|
{ "id": 456, "username": "jane", "avatar": "..." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Обновить профиль
|
||||||
|
```
|
||||||
|
PUT /api/users/me
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "newname",
|
||||||
|
"bio": "Новое описание",
|
||||||
|
"avatar": "https://..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"user": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CHATS
|
||||||
|
|
||||||
|
#### Получить все чаты
|
||||||
|
```
|
||||||
|
GET /api/chats
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"chats": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 789,
|
||||||
"name": "Vision Pro",
|
"type": "private",
|
||||||
"tagline": "Пространственный компьютер здесь",
|
"name": "jane",
|
||||||
"description": "...",
|
"avatar": "...",
|
||||||
"price": 3499,
|
"participants": [123, 456],
|
||||||
"image": "https://...",
|
"lastMessage": {
|
||||||
"features": ["M2", "R1"],
|
"text": "Привет!",
|
||||||
"category": "premium",
|
"createdAt": "2026-08-17T10:30:00Z"
|
||||||
"order": 1
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
#### Создать чат
|
||||||
|
|
||||||
### Получить продукт по ID
|
|
||||||
```
|
|
||||||
GET /api/products/:id
|
|
||||||
```
|
```
|
||||||
|
POST /api/chats
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
**Ответ:**
|
// Приватный чат
|
||||||
```json
|
{
|
||||||
|
"type": "private",
|
||||||
|
"participantId": 456
|
||||||
|
}
|
||||||
|
|
||||||
|
// Группа
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"name": "Моя группа"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Канал
|
||||||
|
{
|
||||||
|
"type": "channel",
|
||||||
|
"name": "Мой канал"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": { /* объект продукта */ }
|
"chat": { ... }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
#### Получить сообщения
|
||||||
|
|
||||||
### Получить активные объявления
|
|
||||||
```
|
|
||||||
GET /api/announcements
|
|
||||||
```
|
```
|
||||||
|
GET /api/chats/:id/messages
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
**Ответ:**
|
Response:
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 111,
|
||||||
"title": "Apple Event 2026",
|
"chatId": 789,
|
||||||
"date": "2026-09-15",
|
"senderId": 456,
|
||||||
"content": "Смотрите презентацию...",
|
"senderName": "jane",
|
||||||
"active": true
|
"senderAvatar": "...",
|
||||||
|
"text": "Привет!", // Уже расшифровано
|
||||||
|
"type": "text",
|
||||||
|
"createdAt": "2026-08-17T10:30:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
#### Отправить сообщение
|
||||||
|
|
||||||
### Получить настройки сайта
|
|
||||||
```
|
|
||||||
GET /api/settings
|
|
||||||
```
|
```
|
||||||
|
POST /api/chats/:id/messages
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
**Ответ:**
|
{
|
||||||
```json
|
"text": "Привет!",
|
||||||
|
"type": "text" // или "emoji", "sticker"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": {
|
"message": { ... }
|
||||||
"heroTitle": "Добро пожаловать в будущее",
|
|
||||||
"heroSubtitle": "Инновации, которые меняют мир",
|
|
||||||
"theme": "dark"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔐 ADMIN API (для редактирования)
|
### GROUPS
|
||||||
|
|
||||||
### Добавить продукт
|
#### Добавить участника в группу
|
||||||
```
|
```
|
||||||
POST /api/admin/products
|
POST /api/groups/:id/members
|
||||||
|
Authorization: Bearer <token>
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "iPhone 18 Pro",
|
"userId": 789
|
||||||
"tagline": "Ещё мощнее",
|
|
||||||
"description": "Описание...",
|
|
||||||
"price": 999,
|
|
||||||
"category": "phones",
|
|
||||||
"image": "https://...",
|
|
||||||
"features": ["A18 чип", "Face ID", "5G"]
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
**Ответ:**
|
Response:
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": { /* созданный продукт с id и order */ }
|
"group": { ... }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Обновить продукт
|
### CHANNELS
|
||||||
```
|
|
||||||
PUT /api/admin/products/:id
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
#### Подписаться на канал
|
||||||
"name": "Новое название",
|
|
||||||
"price": 1099
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
POST /api/channels/:id/subscribe
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
---
|
Response:
|
||||||
|
|
||||||
### Удалить продукт
|
|
||||||
```
|
|
||||||
DELETE /api/admin/products/:id
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ответ:**
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"deleted": 123
|
"channel": { ... }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Добавить объявление
|
## 📊 Структура данных
|
||||||
```
|
|
||||||
POST /api/admin/announcements
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"title": "Специальное предложение",
|
|
||||||
"date": "2026-12-01",
|
|
||||||
"content": "Скидки до 30%",
|
|
||||||
"active": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Обновить объявление
|
|
||||||
```
|
|
||||||
PUT /api/admin/announcements/:id
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"title": "Обновлённый заголовок",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Удалить объявление
|
|
||||||
```
|
|
||||||
DELETE /api/admin/announcements/:id
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Обновить настройки сайта
|
|
||||||
```
|
|
||||||
PUT /api/admin/settings
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"heroTitle": "Новый заголовок",
|
|
||||||
"heroSubtitle": "Новый подзаголовок",
|
|
||||||
"theme": "light"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗂 Структура данных (data.json)
|
|
||||||
|
|
||||||
|
### User
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"products": [
|
"id": 1234567890,
|
||||||
{
|
"username": "john",
|
||||||
"id": number,
|
"email": "john@example.com",
|
||||||
"name": string,
|
"passwordHash": "sha256...",
|
||||||
"tagline": string,
|
"avatar": "https://...",
|
||||||
"description": string,
|
"bio": "О себе",
|
||||||
"price": number,
|
"createdAt": "2026-08-17T10:00:00Z"
|
||||||
"image": string (URL),
|
}
|
||||||
"features": string[],
|
```
|
||||||
"category": string,
|
|
||||||
"order": number
|
### Chat
|
||||||
}
|
```json
|
||||||
],
|
{
|
||||||
"announcements": [
|
"id": 789,
|
||||||
{
|
"type": "private|group|channel",
|
||||||
"id": number,
|
"participants": [123, 456],
|
||||||
"title": string,
|
"name": "Группа",
|
||||||
"date": string (ISO date),
|
"avatar": "https://...",
|
||||||
"content": string,
|
"ownerId": 123,
|
||||||
"active": boolean
|
"createdAt": "2026-08-17T10:00:00Z"
|
||||||
}
|
}
|
||||||
],
|
```
|
||||||
"siteSettings": {
|
|
||||||
"heroTitle": string,
|
### Message
|
||||||
"heroSubtitle": string,
|
```json
|
||||||
"theme": string ("dark" | "light")
|
{
|
||||||
}
|
"id": 111,
|
||||||
|
"chatId": 789,
|
||||||
|
"senderId": 123,
|
||||||
|
"senderName": "john",
|
||||||
|
"senderAvatar": "...",
|
||||||
|
"text": "Привет!",
|
||||||
|
"encrypted": {
|
||||||
|
"iv": "...",
|
||||||
|
"encrypted": "...",
|
||||||
|
"authTag": "..."
|
||||||
|
},
|
||||||
|
"type": "text|emoji|sticker",
|
||||||
|
"createdAt": "2026-08-17T10:30:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Архитектура сервера
|
## 🏗 Архитектура сервера
|
||||||
|
|
||||||
```
|
```
|
||||||
server.js
|
server.js
|
||||||
├── Routes (API)
|
├── Crypto Utils
|
||||||
│ ├── GET /api/products → список продуктов
|
│ ├── hashPassword()
|
||||||
│ ├── GET /api/products/:id → продукт по ID
|
│ ├── generateToken()
|
||||||
│ ├── GET /api/announcements → активные объявления
|
│ ├── verifyToken()
|
||||||
│ ├── GET /api/settings → настройки сайта
|
│ ├── encryptMessage()
|
||||||
|
│ └── decryptMessage()
|
||||||
|
│
|
||||||
|
├── Routes
|
||||||
|
│ ├── AUTH
|
||||||
|
│ │ ├── POST /api/auth/register
|
||||||
|
│ │ ├── POST /api/auth/login
|
||||||
|
│ │ ├── POST /api/auth/logout
|
||||||
|
│ │ └── GET /api/auth/me
|
||||||
│ │
|
│ │
|
||||||
│ └── ADMIN (CRUD)
|
│ ├── USERS
|
||||||
│ ├── POST /api/admin/products
|
│ │ ├── GET /api/users
|
||||||
│ ├── PUT /api/admin/products/:id
|
│ │ └── PUT /api/users/me
|
||||||
│ ├── DELETE /api/admin/products/:id
|
│ │
|
||||||
│ ├── POST /api/admin/announcements
|
│ ├── CHATS
|
||||||
│ ├── PUT /api/admin/announcements/:id
|
│ │ ├── GET /api/chats
|
||||||
│ ├── DELETE /api/admin/announcements/:id
|
│ │ ├── POST /api/chats
|
||||||
│ └── PUT /api/admin/settings
|
│ │ ├── GET /api/chats/:id/messages
|
||||||
|
│ │ └── POST /api/chats/:id/messages
|
||||||
|
│ │
|
||||||
|
│ ├── GROUPS
|
||||||
|
│ │ └── POST /api/groups/:id/members
|
||||||
|
│ │
|
||||||
|
│ └── CHANNELS
|
||||||
|
│ └── POST /api/channels/:id/subscribe
|
||||||
│
|
│
|
||||||
└── Static Files
|
└── Static Files
|
||||||
├── / → index.html
|
├── index.html (лендинг + auth)
|
||||||
├── /admin.html
|
├── app.html (мессенджер)
|
||||||
├── /style.css
|
├── landing.css
|
||||||
├── /app.js
|
├── landing.js
|
||||||
└── /admin.js
|
├── app.css
|
||||||
|
└── app.js
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Примеры использования (Frontend)
|
## 📱 Мобильное приложение (Future)
|
||||||
|
|
||||||
### Загрузка продуктов
|
API полностью готов для подключения мобильного приложения:
|
||||||
```javascript
|
|
||||||
async function loadProducts() {
|
### React Native / Flutter интеграция
|
||||||
const res = await fetch('/api/products');
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.ok) {
|
|
||||||
console.log(json.data); // массив продуктов
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Добавление продукта (из админки)
|
|
||||||
```javascript
|
```javascript
|
||||||
async function addProduct(data) {
|
// Пример для React Native
|
||||||
const res = await fetch('/api/admin/products', {
|
const API_BASE = 'https://your-domain.com';
|
||||||
|
|
||||||
|
async function login(email, password) {
|
||||||
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify({ email, password })
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
return json.ok;
|
|
||||||
|
if (json.ok) {
|
||||||
|
await AsyncStorage.setItem('token', json.token);
|
||||||
|
return json.user;
|
||||||
|
}
|
||||||
|
throw new Error(json.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getChats(token) {
|
||||||
|
const res = await fetch(`${API_BASE}/api/chats`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
return json.chats;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔒 Безопасность (TODO для продакшена)
|
## 🔒 Безопасность (Production Checklist)
|
||||||
|
|
||||||
Сейчас API открыт. Для продакшена добавить:
|
Для продакшена добавить:
|
||||||
- JWT или session аутентификацию для `/api/admin/*`
|
|
||||||
- Валидацию входных данных
|
- [ ] HTTPS (TLS 1.3)
|
||||||
- Rate limiting
|
- [ ] Rate limiting (100 req/min на IP)
|
||||||
- CORS только для доверенных доменов
|
- [ ] Helmet.js для security headers
|
||||||
|
- [ ] Валидация входных данных (Joi/Zod)
|
||||||
|
- [ ] 2FA (TOTP)
|
||||||
|
- [ ] Сессионные токены с refresh
|
||||||
|
- [ ] Блокировка при подозрительной активности
|
||||||
|
- [ ] Аудит логов
|
||||||
|
- [ ] GDPR compliance
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Тестирование
|
## 🧪 Тестирование API
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Получить все продукты
|
# Регистрация
|
||||||
curl http://localhost:3000/api/products
|
curl -X POST http://localhost:3000/api/auth/register \
|
||||||
|
|
||||||
# Добавить продукт
|
|
||||||
curl -X POST http://localhost:3000/api/admin/products \
|
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"name":"Test","tagline":"Test","price":99,"category":"test","features":[]}'
|
-d '{"username":"test","email":"test@test.com","password":"123456"}'
|
||||||
|
|
||||||
# Обновить настройки
|
# Вход
|
||||||
curl -X PUT http://localhost:3000/api/admin/settings \
|
curl -X POST http://localhost:3000/api/auth/login \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"heroTitle":"Hello"}'
|
-d '{"email":"test@test.com","password":"123456"}'
|
||||||
|
|
||||||
|
# Получить профиль
|
||||||
|
curl -X GET http://localhost:3000/api/auth/me \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
|
||||||
|
# Создать чат
|
||||||
|
curl -X POST http://localhost:3000/api/chats \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"group","name":"Test Group"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Масштабирование
|
||||||
|
|
||||||
|
### Текущая архитектура (JSON файлы)
|
||||||
|
- ✅ Подходит для: до 1000 пользователей, до 10K сообщений
|
||||||
|
- ⚠️ Ограничения: нет real-time, нет шардирования
|
||||||
|
|
||||||
|
### Roadmap для продакшена
|
||||||
|
|
||||||
|
1. **База данных:** PostgreSQL + Prisma
|
||||||
|
2. **Real-time:** WebSocket (Socket.io)
|
||||||
|
3. **Кеширование:** Redis (сессии, последние сообщения)
|
||||||
|
4. **Файлы:** S3-compatible storage (аватарки, файлы)
|
||||||
|
5. **Очереди:** RabbitMQ/Kafka для асинхронных задач
|
||||||
|
6. **Мониторинг:** Prometheus + Grafana
|
||||||
|
7. **Контейнеризация:** Docker + Kubernetes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Поддержка
|
||||||
|
|
||||||
|
API Version: 1.0.0
|
||||||
|
Last Updated: 2026-08-17
|
||||||
|
|||||||
682
app.css
Normal file
682
app.css
Normal file
@ -0,0 +1,682 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 360px 1fr;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
background: #1d1d1f;
|
||||||
|
border-right: 1px solid rgba(255,255,255,0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info:hover {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-new-chat {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-new-chat:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item {
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item:hover {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item.active {
|
||||||
|
background: rgba(102, 126, 234, 0.15);
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-last {
|
||||||
|
color: #86868b;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6e6e73;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-type-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 980px;
|
||||||
|
background: rgba(102, 126, 234, 0.2);
|
||||||
|
color: #667eea;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Area */
|
||||||
|
.chat-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: #1d1d1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-avatar {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-info h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.online {
|
||||||
|
color: #34c759;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.messages-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-chat h3 {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #f5f5f7;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 70%;
|
||||||
|
animation: messageSlide 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageSlide {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent {
|
||||||
|
align-self: flex-end;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
background: #1d1d1f;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .message-content {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .message-sender {
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #86868b;
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .message-time {
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Input */
|
||||||
|
.message-input-container {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: #1d1d1f;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-emoji {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-emoji:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 24px;
|
||||||
|
background: #1d1d1f;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
display: none;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.4);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 980px;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-send {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-send:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #1d1d1f;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 32px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
position: relative;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #86868b;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #86868b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:hover {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
.settings-content {
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-change-avatar {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
border-radius: 980px;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-change-avatar:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #86868b;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #ff3b30;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #ff3b30;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
background: #ff3b30;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.mobile-active {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
app.html
Normal file
129
app.html
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>VibeChat — Мессенджер</title>
|
||||||
|
<link rel="stylesheet" href="app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="user-info" onclick="openSettings()">
|
||||||
|
<img src="" alt="" class="user-avatar" id="currentUserAvatar">
|
||||||
|
<span class="user-name" id="currentUserName"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-new-chat" onclick="showNewChatModal()">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" placeholder="Поиск" id="searchInput" oninput="filterChats()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-list" id="chatList">
|
||||||
|
<!-- Chats will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Chat Area -->
|
||||||
|
<main class="chat-area">
|
||||||
|
<div class="chat-header" id="chatHeader">
|
||||||
|
<div class="chat-header-info">
|
||||||
|
<img src="" alt="" class="chat-header-avatar" id="chatHeaderAvatar">
|
||||||
|
<div>
|
||||||
|
<h3 id="chatHeaderName"></h3>
|
||||||
|
<p id="chatHeaderStatus" class="status"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="messages-container" id="messagesContainer">
|
||||||
|
<div class="empty-chat" id="emptyChat">
|
||||||
|
<div class="empty-icon">💬</div>
|
||||||
|
<h3>Выберите чат</h3>
|
||||||
|
<p>Чтобы начать общение, выберите чат из списка слева</p>
|
||||||
|
</div>
|
||||||
|
<div class="messages-list" id="messagesList" style="display:none">
|
||||||
|
<!-- Messages will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-input-container" id="messageInputContainer" style="display:none">
|
||||||
|
<button class="btn-emoji" onclick="toggleEmojiPicker()">😀</button>
|
||||||
|
<div class="emoji-picker" id="emojiPicker">
|
||||||
|
<div class="emoji-grid">
|
||||||
|
${['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'].map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="message-input" id="messageInput" placeholder="Напишите сообщение..." onkeypress="handleKeyPress(event)">
|
||||||
|
<button class="btn-send" onclick="sendMessage()">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Chat Modal -->
|
||||||
|
<div id="newChatModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="modal-close" onclick="closeNewChatModal()">×</button>
|
||||||
|
<h2>Новый чат</h2>
|
||||||
|
|
||||||
|
<div class="modal-tabs">
|
||||||
|
<button class="tab active" onclick="switchTab('chat')">Чат</button>
|
||||||
|
<button class="tab" onclick="switchTab('group')">Группа</button>
|
||||||
|
<button class="tab" onclick="switchTab('channel')">Канал</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chatTab">
|
||||||
|
<div class="user-list" id="userList">
|
||||||
|
<!-- Users will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="groupTab" style="display:none">
|
||||||
|
<input type="text" id="groupName" placeholder="Название группы" class="modal-input">
|
||||||
|
<button class="btn-submit" onclick="createGroup()">Создать группу</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="channelTab" style="display:none">
|
||||||
|
<input type="text" id="channelName" placeholder="Название канала" class="modal-input">
|
||||||
|
<button class="btn-submit" onclick="createChannel()">Создать канал</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div id="settingsModal" class="modal">
|
||||||
|
<div class="modal-content settings-content">
|
||||||
|
<button class="modal-close" onclick="closeSettings()">×</button>
|
||||||
|
<h2>Настройки</h2>
|
||||||
|
|
||||||
|
<div class="settings-avatar">
|
||||||
|
<img src="" alt="" id="settingsAvatar">
|
||||||
|
<button class="btn-change-avatar" onclick="changeAvatar()">Изменить</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onsubmit="updateProfile(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Имя пользователя</label>
|
||||||
|
<input type="text" id="settingsUsername" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>О себе</label>
|
||||||
|
<textarea id="settingsBio" rows="3" placeholder="Расскажите о себе"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-submit">Сохранить</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button class="btn-logout" onclick="logout()">Выйти из аккаунта</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
581
app.js
581
app.js
@ -1,166 +1,473 @@
|
|||||||
// ===================================
|
// VibeChat Messenger App
|
||||||
// APPLE-STYLE APP - FRONTEND
|
|
||||||
// ===================================
|
|
||||||
|
|
||||||
const API = {
|
const API = {
|
||||||
products: '/api/products',
|
me: '/api/auth/me',
|
||||||
announcements: '/api/announcements',
|
logout: '/api/auth/logout',
|
||||||
settings: '/api/settings'
|
users: '/api/users',
|
||||||
|
chats: '/api/chats',
|
||||||
|
updateProfile: '/api/users/me'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load settings and content
|
let currentUser = null;
|
||||||
async function init() {
|
let currentChat = null;
|
||||||
await loadSettings();
|
let allChats = [];
|
||||||
await loadAnnouncements();
|
let allUsers = [];
|
||||||
await loadProducts();
|
|
||||||
setupScrollEffects();
|
|
||||||
setupRevealAnimations();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load site settings
|
// Initialize app
|
||||||
async function loadSettings() {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
const token = localStorage.getItem('vibechat_token');
|
||||||
const res = await fetch(API.settings);
|
if (!token) {
|
||||||
const json = await res.json();
|
window.location.href = 'index.html';
|
||||||
if (json.ok && json.data) {
|
|
||||||
document.getElementById('heroTitle').textContent = json.data.heroTitle || 'Добро пожаловать';
|
|
||||||
document.getElementById('heroSubtitle').textContent = json.data.heroSubtitle || 'Инновации здесь';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load settings:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load announcements
|
|
||||||
async function loadAnnouncements() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(API.announcements);
|
|
||||||
const json = await res.json();
|
|
||||||
const bar = document.getElementById('announcementBar');
|
|
||||||
if (json.ok && json.data.length > 0) {
|
|
||||||
const ann = json.data[0];
|
|
||||||
bar.innerHTML = `${ann.title} — ${ann.content} <a href="#">Подробнее →</a>`;
|
|
||||||
} else {
|
|
||||||
bar.classList.add('hidden');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load announcements:', e);
|
|
||||||
document.getElementById('announcementBar').classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load products
|
|
||||||
async function loadProducts() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(API.products);
|
|
||||||
const json = await res.json();
|
|
||||||
const container = document.getElementById('productsContainer');
|
|
||||||
if (!json.ok || !json.data) {
|
|
||||||
container.innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Нет продуктов</p>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = json.data.map((p, i) => `
|
await loadCurrentUser();
|
||||||
<div class="product-card" style="transition-delay: ${i * 100}ms">
|
await loadChats();
|
||||||
<img src="${p.image}" alt="${p.name}" class="product-image">
|
await loadUsers();
|
||||||
<div class="product-content">
|
|
||||||
<h3 class="product-name">${p.name}</h3>
|
// Inject emoji picker HTML
|
||||||
<p class="product-tagline">${p.tagline}</p>
|
const emojis = ['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'];
|
||||||
<p class="product-price">от ${p.price} $</p>
|
document.getElementById('emojiPicker').innerHTML = `
|
||||||
<div class="product-features">
|
<div class="emoji-grid">
|
||||||
${p.features.map(f => `<span class="feature-tag">${f}</span>`).join('')}
|
${emojis.map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load current user
|
||||||
|
async function loadCurrentUser() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.me, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (!json.ok) {
|
||||||
|
localStorage.removeItem('vibechat_token');
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser = json.user;
|
||||||
|
localStorage.setItem('vibechat_user', JSON.stringify(currentUser));
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
document.getElementById('currentUserName').textContent = currentUser.username;
|
||||||
|
document.getElementById('currentUserAvatar').src = currentUser.avatar;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load user:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load chats
|
||||||
|
async function loadChats() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.chats, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
allChats = json.chats;
|
||||||
|
renderChatList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load chats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render chat list
|
||||||
|
function renderChatList() {
|
||||||
|
const chatList = document.getElementById('chatList');
|
||||||
|
|
||||||
|
if (allChats.length === 0) {
|
||||||
|
chatList.innerHTML = `
|
||||||
|
<div style="padding:40px 20px;text-align:center;color:#86868b">
|
||||||
|
<p>Нет чатов</p>
|
||||||
|
<p style="font-size:13px;margin-top:8px">Нажмите + чтобы создать новый</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatList.innerHTML = allChats.map(chat => `
|
||||||
|
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
|
||||||
|
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
|
||||||
|
<div class="chat-item-info">
|
||||||
|
<div class="chat-item-name">
|
||||||
|
${chat.name}
|
||||||
|
${chat.type !== 'private' ? `<span class="chat-type-badge">${chat.type === 'group' ? 'Группа' : 'Канал'}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="chat-item-last">
|
||||||
|
${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${chat.lastMessage ? `<span class="chat-item-time">${formatTime(chat.lastMessage.createdAt)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load users for new chat
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.users, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
allUsers = json.users;
|
||||||
|
renderUserList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load users:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render user list
|
||||||
|
function renderUserList() {
|
||||||
|
const userList = document.getElementById('userList');
|
||||||
|
|
||||||
|
if (allUsers.length === 0) {
|
||||||
|
userList.innerHTML = '<p style="padding:20px;text-align:center;color:#86868b">Нет пользователей</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userList.innerHTML = allUsers.map(user => `
|
||||||
|
<div class="user-item" onclick="createPrivateChat(${user.id})">
|
||||||
|
<img src="${user.avatar}" alt="" class="user-item-avatar">
|
||||||
|
<span class="user-item-name">${user.username}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select chat
|
||||||
|
async function selectChat(chatId) {
|
||||||
|
currentChat = allChats.find(c => c.id === chatId);
|
||||||
|
if (!currentChat) return;
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
document.getElementById('emptyChat').style.display = 'none';
|
||||||
|
document.getElementById('messagesList').style.display = 'flex';
|
||||||
|
document.getElementById('messageInputContainer').style.display = 'block';
|
||||||
|
|
||||||
|
// Update header
|
||||||
|
document.getElementById('chatHeaderName').textContent = currentChat.name;
|
||||||
|
document.getElementById('chatHeaderAvatar').src = currentChat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + currentChat.id;
|
||||||
|
document.getElementById('chatHeaderStatus').textContent = currentChat.type === 'channel' ? 'Канал' : (currentChat.type === 'group' ? `${currentChat.participants?.length || 0} участников` : 'Онлайн');
|
||||||
|
|
||||||
|
// Render chat list again to update active state
|
||||||
|
renderChatList();
|
||||||
|
|
||||||
|
// Load messages
|
||||||
|
await loadMessages(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load messages
|
||||||
|
async function loadMessages(chatId) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(`${API.chats}/${chatId}/messages`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
renderMessages(json.messages);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load messages:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render messages
|
||||||
|
function renderMessages(messages) {
|
||||||
|
const messagesList = document.getElementById('messagesList');
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
messagesList.innerHTML = '<p style="text-align:center;color:#86868b;padding:40px">Сообщений пока нет</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesList.innerHTML = messages.map(msg => `
|
||||||
|
<div class="message ${msg.senderId === currentUser.id ? 'sent' : ''}">
|
||||||
|
<img src="${msg.senderAvatar}" alt="" class="message-avatar">
|
||||||
|
<div class="message-content">
|
||||||
|
${msg.senderId !== currentUser.id ? `<div class="message-sender">${msg.senderName}</div>` : ''}
|
||||||
|
<div class="message-text">${escapeHtml(msg.text)}</div>
|
||||||
|
<div class="message-time">${formatTime(msg.createdAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
// Trigger visibility check after load
|
// Scroll to bottom
|
||||||
setTimeout(checkProductVisibility, 100);
|
const container = document.getElementById('messagesContainer');
|
||||||
} catch (e) {
|
container.scrollTop = container.scrollHeight;
|
||||||
console.error('Failed to load products:', e);
|
|
||||||
document.getElementById('productsContainer').innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Ошибка загрузки</p>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll effects for navigation
|
// Send message
|
||||||
function setupScrollEffects() {
|
async function sendMessage() {
|
||||||
const nav = document.getElementById('nav');
|
const input = document.getElementById('messageInput');
|
||||||
const heroBg = document.getElementById('heroBg');
|
const text = input.value.trim();
|
||||||
const featuresBg = document.getElementById('featuresBg');
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
if (!text || !currentChat) return;
|
||||||
const scrolled = window.pageYOffset;
|
|
||||||
|
|
||||||
// Nav background
|
try {
|
||||||
if (scrolled > 100) {
|
const token = localStorage.getItem('vibechat_token');
|
||||||
nav.classList.add('scrolled');
|
const res = await fetch(`${API.chats}/${currentChat.id}/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text })
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
input.value = '';
|
||||||
|
await loadMessages(currentChat.id);
|
||||||
|
await loadChats(); // Update last message
|
||||||
} else {
|
} else {
|
||||||
nav.classList.remove('scrolled');
|
alert('Ошибка отправки: ' + json.error);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
// Hero parallax
|
alert('Ошибка: ' + e.message);
|
||||||
if (scrolled < window.innerHeight) {
|
|
||||||
heroBg.style.transform = `translateY(${scrolled * 0.5}px) scale(${1 + scrolled * 0.0008})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Features parallax
|
|
||||||
const featuresSection = document.getElementById('features');
|
|
||||||
if (featuresSection) {
|
|
||||||
const rect = featuresSection.getBoundingClientRect();
|
|
||||||
if (rect.top < window.innerHeight && rect.bottom > 0) {
|
|
||||||
const offset = (window.innerHeight - rect.top) * 0.2;
|
|
||||||
featuresBg.style.transform = `translateY(${offset}px)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reveal animations on scroll
|
// Handle enter key
|
||||||
function setupRevealAnimations() {
|
function handleKeyPress(event) {
|
||||||
const reveals = document.querySelectorAll('.reveal');
|
if (event.key === 'Enter') {
|
||||||
|
sendMessage();
|
||||||
const checkReveal = () => {
|
|
||||||
reveals.forEach(el => {
|
|
||||||
const top = el.getBoundingClientRect().top;
|
|
||||||
const trigger = window.innerHeight * 0.85;
|
|
||||||
if (top < trigger) {
|
|
||||||
el.classList.add('active');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', checkReveal);
|
|
||||||
checkReveal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check product cards visibility
|
// Emoji picker
|
||||||
function checkProductVisibility() {
|
function toggleEmojiPicker() {
|
||||||
const cards = document.querySelectorAll('.product-card');
|
const picker = document.getElementById('emojiPicker');
|
||||||
const observer = new IntersectionObserver((entries) => {
|
picker.classList.toggle('active');
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('visible');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.1 });
|
|
||||||
|
|
||||||
cards.forEach(card => observer.observe(card));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth scroll for anchor links
|
function insertEmoji(emoji) {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const input = document.getElementById('messageInput');
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
input.value += emoji;
|
||||||
anchor.addEventListener('click', function(e) {
|
input.focus();
|
||||||
const href = this.getAttribute('href');
|
document.getElementById('emojiPicker').classList.remove('active');
|
||||||
if (href !== '#') {
|
}
|
||||||
e.preventDefault();
|
|
||||||
const target = document.querySelector(href);
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
init();
|
// Close emoji picker on outside click
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const picker = document.getElementById('emojiPicker');
|
||||||
|
const btn = document.querySelector('.btn-emoji');
|
||||||
|
if (!picker.contains(e.target) && !btn.contains(e.target)) {
|
||||||
|
picker.classList.remove('active');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// New chat modal
|
||||||
|
function showNewChatModal() {
|
||||||
|
document.getElementById('newChatModal').classList.add('active');
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNewChatModal() {
|
||||||
|
document.getElementById('newChatModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tab) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
document.getElementById('chatTab').style.display = tab === 'chat' ? 'block' : 'none';
|
||||||
|
document.getElementById('groupTab').style.display = tab === 'group' ? 'block' : 'none';
|
||||||
|
document.getElementById('channelTab').style.display = tab === 'channel' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create private chat
|
||||||
|
async function createPrivateChat(userId) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.chats, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ type: 'private', participantId: userId })
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
closeNewChatModal();
|
||||||
|
await loadChats();
|
||||||
|
selectChat(json.chat.id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create group
|
||||||
|
async function createGroup() {
|
||||||
|
const name = document.getElementById('groupName').value.trim();
|
||||||
|
if (!name) return alert('Введите название группы');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.chats, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ type: 'group', name })
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
closeNewChatModal();
|
||||||
|
document.getElementById('groupName').value = '';
|
||||||
|
await loadChats();
|
||||||
|
selectChat(json.chat.id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create channel
|
||||||
|
async function createChannel() {
|
||||||
|
const name = document.getElementById('channelName').value.trim();
|
||||||
|
if (!name) return alert('Введите название канала');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.chats, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ type: 'channel', name })
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
closeNewChatModal();
|
||||||
|
document.getElementById('channelName').value = '';
|
||||||
|
await loadChats();
|
||||||
|
selectChat(json.chat.id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
function openSettings() {
|
||||||
|
const user = JSON.parse(localStorage.getItem('vibechat_user'));
|
||||||
|
document.getElementById('settingsAvatar').src = user.avatar;
|
||||||
|
document.getElementById('settingsUsername').value = user.username;
|
||||||
|
document.getElementById('settingsBio').value = user.bio || '';
|
||||||
|
document.getElementById('settingsModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettings() {
|
||||||
|
document.getElementById('settingsModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProfile(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('settingsUsername').value;
|
||||||
|
const bio = document.getElementById('settingsBio').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
const res = await fetch(API.updateProfile, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, bio })
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
||||||
|
currentUser = json.user;
|
||||||
|
document.getElementById('currentUserName').textContent = currentUser.username;
|
||||||
|
document.getElementById('currentUserAvatar').src = currentUser.avatar;
|
||||||
|
closeSettings();
|
||||||
|
alert('Профиль обновлён! ✅');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAvatar() {
|
||||||
|
const url = prompt('Введите URL аватара:', currentUser.avatar);
|
||||||
|
if (url) {
|
||||||
|
updateProfile({ preventDefault: () => {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
async function logout() {
|
||||||
|
if (!confirm('Вы уверены, что хотите выйти?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
await fetch(API.logout, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
localStorage.removeItem('vibechat_token');
|
||||||
|
localStorage.removeItem('vibechat_user');
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter chats
|
||||||
|
function filterChats() {
|
||||||
|
const query = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
const filtered = allChats.filter(c => c.name.toLowerCase().includes(query));
|
||||||
|
|
||||||
|
const chatList = document.getElementById('chatList');
|
||||||
|
chatList.innerHTML = filtered.map(chat => `
|
||||||
|
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
|
||||||
|
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
|
||||||
|
<div class="chat-item-info">
|
||||||
|
<div class="chat-item-name">${chat.name}</div>
|
||||||
|
<div class="chat-item-last">${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function formatTime(isoString) {
|
||||||
|
const date = new Date(isoString);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
|
||||||
|
if (diff < 60000) return 'Только что';
|
||||||
|
if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад';
|
||||||
|
if (diff < 86400000) return Math.floor(diff / 3600000) + ' ч назад';
|
||||||
|
if (diff < 604800000) return Math.floor(diff / 86400000) + ' дн назад';
|
||||||
|
|
||||||
|
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|||||||
66
data.json
66
data.json
@ -1,62 +1,8 @@
|
|||||||
{
|
{
|
||||||
"products": [
|
"users": [],
|
||||||
{
|
"chats": [],
|
||||||
"id": 1,
|
"messages": [],
|
||||||
"name": "Vision Pro",
|
"groups": [],
|
||||||
"tagline": "Пространственный компьютер здесь",
|
"channels": [],
|
||||||
"description": "Революционный интерфейс между человеком и машиной. Погрузитесь в мир цифровых возможностей.",
|
"sessions": []
|
||||||
"price": 3499,
|
|
||||||
"image": "https://picsum.photos/800/600?random=1",
|
|
||||||
"features": ["M2 чип", "R1 сопроцессор", "23 млн пикселей", "visionOS"],
|
|
||||||
"category": "premium",
|
|
||||||
"order": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "AirPods Max",
|
|
||||||
"tagline": "Звук в новом измерении",
|
|
||||||
"description": "Высокая точность звука. Активное шумоподавление. Пространственное аудио.",
|
|
||||||
"price": 549,
|
|
||||||
"image": "https://picsum.photos/800/600?random=2",
|
|
||||||
"features": ["ANC", "40ч работы", "Пространственное аудио", "H1 чип"],
|
|
||||||
"category": "audio",
|
|
||||||
"order": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "Apple Watch Ultra",
|
|
||||||
"tagline": "Приключения ждут",
|
|
||||||
"description": "Самые прочные и функциональные часы для экстремальных условий.",
|
|
||||||
"price": 799,
|
|
||||||
"image": "https://picsum.photos/800/600?random=3",
|
|
||||||
"features": ["49мм титан", "36ч работы", "GPS", "100м водолазные"],
|
|
||||||
"category": "wearables",
|
|
||||||
"order": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"name": "MacBook Pro 16",
|
|
||||||
"tagline": "Мощность профессионала",
|
|
||||||
"description": "Чип M3 Max. Невероятная производительность для самых требовательных задач.",
|
|
||||||
"price": 2499,
|
|
||||||
"image": "https://picsum.photos/800/600?random=4",
|
|
||||||
"features": ["M3 Max", "128GB RAM", "8TB SSD", "22ч работы"],
|
|
||||||
"category": "computers",
|
|
||||||
"order": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"announcements": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Apple Event 2026",
|
|
||||||
"date": "2026-09-15",
|
|
||||||
"content": "Смотрите презентацию новых продуктов в прямом эфире",
|
|
||||||
"active": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"siteSettings": {
|
|
||||||
"heroTitle": "Добро пожаловать в будущее",
|
|
||||||
"heroSubtitle": "Инновации, которые меняют мир",
|
|
||||||
"theme": "dark"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
245
index.html
245
index.html
@ -3,171 +3,136 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Apple Style — Будущее здесь</title>
|
<title>VibeChat — Мессенджер будущего</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="landing.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Announcement Bar -->
|
<!-- Landing Page -->
|
||||||
<div id="announcementBar" class="announcement-bar"></div>
|
<div id="landingPage">
|
||||||
|
<nav class="landing-nav">
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="nav" id="nav">
|
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
<a href="/" class="logo"></a>
|
<div class="logo">💬 VibeChat</div>
|
||||||
<div class="nav-links">
|
<button class="btn-login" onclick="showAuth('login')">Войти</button>
|
||||||
<a href="#products">Продукты</a>
|
|
||||||
<a href="#features">Возможности</a>
|
|
||||||
<a href="#vision">Vision</a>
|
|
||||||
<a href="admin.html" class="admin-link">Админка</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<section class="hero">
|
||||||
<section class="hero" id="hero">
|
|
||||||
<div class="hero-bg" id="heroBg"></div>
|
|
||||||
<div class="hero-overlay"></div>
|
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1 id="heroTitle" class="hero-title"></h1>
|
<h1>Общение без границ</h1>
|
||||||
<p id="heroSubtitle" class="hero-subtitle"></p>
|
<p>Быстрый. Безопасный. С End-to-End шифрованием.</p>
|
||||||
<div class="hero-cta">
|
<div class="hero-buttons">
|
||||||
<a href="#products" class="btn btn-primary">Смотреть продукты</a>
|
<button class="btn-primary" onclick="showAuth('register')">Начать бесплатно</button>
|
||||||
<a href="#vision" class="btn btn-secondary">Узнать больше</a>
|
<button class="btn-secondary" onclick="scrollToFeatures()">Узнать больше</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scroll-indicator">
|
<div class="hero-image">
|
||||||
<div class="mouse">
|
<div class="chat-mockup">
|
||||||
<div class="wheel"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Product Showcase (Apple-style scroll) -->
|
|
||||||
<section class="product-showcase" id="products">
|
|
||||||
<div class="showcase-intro">
|
|
||||||
<h2 class="showcase-title">Выберите свой путь</h2>
|
|
||||||
<p class="showcase-subtitle">Инновации в каждой детали</p>
|
|
||||||
</div>
|
|
||||||
<div class="products-container" id="productsContainer">
|
|
||||||
<!-- Products will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Features Section with Parallax -->
|
|
||||||
<section class="features" id="features">
|
<section class="features" id="features">
|
||||||
<div class="features-bg" id="featuresBg"></div>
|
<h2>Почему VibeChat?</h2>
|
||||||
<div class="features-overlay"></div>
|
|
||||||
<div class="features-content">
|
|
||||||
<h2 class="section-title reveal">Технологии будущего</h2>
|
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
<div class="feature-card reveal">
|
<div class="feature-card">
|
||||||
<div class="feature-icon">🚀</div>
|
|
||||||
<h3>Производительность</h3>
|
|
||||||
<p>Чипы M-серии меняют правила игры</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card reveal">
|
|
||||||
<div class="feature-icon">🔒</div>
|
<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>
|
<h3>Безопасность</h3>
|
||||||
<p>Защита данных на аппаратном уровне</p>
|
<p>Двухфакторная аутентификация и секретные чаты</p>
|
||||||
</div>
|
|
||||||
<div class="feature-card reveal">
|
|
||||||
<div class="feature-icon">🌱</div>
|
|
||||||
<h3>Экология</h3>
|
|
||||||
<p>Углеродная нейтральность к 2030</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card reveal">
|
|
||||||
<div class="feature-icon">🤖</div>
|
|
||||||
<h3>ИИ</h3>
|
|
||||||
<p>Apple Intelligence в каждом устройстве</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Vision Section -->
|
|
||||||
<section class="vision" id="vision">
|
|
||||||
<div class="vision-content">
|
|
||||||
<h2 class="vision-title reveal">Vision Pro</h2>
|
|
||||||
<p class="vision-subtitle reveal">Эра пространственных вычислений наступила</p>
|
|
||||||
<div class="vision-features reveal">
|
|
||||||
<div class="vision-feature">
|
|
||||||
<span class="vision-feature-icon">👁️</span>
|
|
||||||
<div>
|
|
||||||
<h4>eyeSight</h4>
|
|
||||||
<p>Оставайтесь на связи с окружающими</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="vision-feature">
|
|
||||||
<span class="vision-feature-icon">🎭</span>
|
|
||||||
<div>
|
|
||||||
<h4>visionOS</h4>
|
|
||||||
<p>Интуитивный пространственный интерфейс</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="vision-feature">
|
|
||||||
<span class="vision-feature-icon">🎬</span>
|
|
||||||
<div>
|
|
||||||
<h4>Immersion</h4>
|
|
||||||
<p>Погружение как в кинотеатре</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="vision-image reveal">
|
|
||||||
<img src="https://picsum.photos/1200/800?random=50" alt="Vision Pro">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Big Image Scroll Section -->
|
|
||||||
<section class="scroll-gallery" id="gallery">
|
|
||||||
<div class="gallery-item" id="galleryItem1">
|
|
||||||
<img src="https://picsum.photos/1920/1080?random=60" alt="">
|
|
||||||
<div class="gallery-caption">
|
|
||||||
<h3>Дизайн</h3>
|
|
||||||
<p>Каждая линия имеет значение</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item" id="galleryItem2">
|
|
||||||
<img src="https://picsum.photos/1920/1080?random=61" alt="">
|
|
||||||
<div class="gallery-caption">
|
|
||||||
<h3>Инновации</h3>
|
|
||||||
<p>Технологии, которые вдохновляют</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item" id="galleryItem3">
|
|
||||||
<img src="https://picsum.photos/1920/1080?random=62" alt="">
|
|
||||||
<div class="gallery-caption">
|
|
||||||
<h3>Будущее</h3>
|
|
||||||
<p>Создаём завтрашний день сегодня</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA Section -->
|
<section class="cta">
|
||||||
<section class="cta-section">
|
<h2>Готовы начать?</h2>
|
||||||
<h2 class="cta-title reveal">Готовы к будущему?</h2>
|
<p>Присоединяйтесь к тысячам пользователей уже сегодня</p>
|
||||||
<p class="cta-subtitle reveal">Откройте для себя мир инноваций уже сегодня</p>
|
<button class="btn-primary" onclick="showAuth('register')">Создать аккаунт</button>
|
||||||
<div class="cta-buttons reveal">
|
|
||||||
<a href="#products" class="btn btn-primary">Выбрать устройство</a>
|
|
||||||
<a href="#" class="btn btn-secondary">Связаться с нами</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="footer-content">
|
<p>© 2026 VibeChat. Все права защищены.</p>
|
||||||
<div class="footer-links">
|
|
||||||
<a href="#">О компании</a>
|
|
||||||
<a href="#">Карьера</a>
|
|
||||||
<a href="#">Инвесторам</a>
|
|
||||||
<a href="#">Новости</a>
|
|
||||||
<a href="#">Конфиденциальность</a>
|
|
||||||
<a href="#">Контакты</a>
|
|
||||||
</div>
|
|
||||||
<p class="copyright">© 2026 Apple Style Demo. Все права защищены.</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<!-- 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>
|
||||||
|
|
||||||
|
<script src="landing.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
433
landing.css
Normal file
433
landing.css
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Landing Navigation */
|
||||||
|
.landing-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px 40px;
|
||||||
|
background: rgba(10, 10, 10, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
z-index: 100;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
border-radius: 980px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
border-color: white;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 140px 40px 60px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content h1 {
|
||||||
|
font-size: clamp(48px, 6vw, 80px);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #a0a0a5 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content p {
|
||||||
|
font-size: clamp(20px, 3vw, 24px);
|
||||||
|
color: #86868b;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
padding: 16px 32px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 980px;
|
||||||
|
color: white;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 16px 32px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid rgba(255,255,255,0.2);
|
||||||
|
border-radius: 980px;
|
||||||
|
color: white;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Mockup */
|
||||||
|
.hero-image {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-mockup {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
background: rgba(29, 29, 31, 0.8);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
box-shadow: 0 40px 80px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mockup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mockup-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mockup-lines {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
height: 12px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line.short {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mockup-messages {
|
||||||
|
padding-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 70%;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.received {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Features */
|
||||||
|
.features {
|
||||||
|
padding: 140px 40px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features h2 {
|
||||||
|
font-size: clamp(36px, 5vw, 56px);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(29, 29, 31, 0.6);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px 32px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
border-color: rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: #86868b;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA */
|
||||||
|
.cta {
|
||||||
|
padding: 140px 40px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(180deg, transparent 0%, rgba(29, 29, 31, 0.5) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta h2 {
|
||||||
|
font-size: clamp(36px, 5vw, 56px);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta p {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #86868b;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #86868b;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #1d1d1f;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
position: relative;
|
||||||
|
animation: modalSlide 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlide {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #86868b;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #86868b;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
color: #86868b;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding-top: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-nav {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features, .cta {
|
||||||
|
padding: 80px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
landing.js
Normal file
107
landing.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// VibeChat Landing Page JavaScript
|
||||||
|
|
||||||
|
const API = {
|
||||||
|
register: '/api/auth/register',
|
||||||
|
login: '/api/auth/login'
|
||||||
|
};
|
||||||
|
|
||||||
|
let authToken = null;
|
||||||
|
|
||||||
|
// Show auth modal
|
||||||
|
function showAuth(type) {
|
||||||
|
const modal = document.getElementById('authModal');
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const registerForm = document.getElementById('registerForm');
|
||||||
|
|
||||||
|
modal.classList.add('active');
|
||||||
|
|
||||||
|
if (type === 'login') {
|
||||||
|
loginForm.style.display = 'block';
|
||||||
|
registerForm.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
loginForm.style.display = 'none';
|
||||||
|
registerForm.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
function closeAuth() {
|
||||||
|
document.getElementById('authModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
document.getElementById('authModal')?.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'authModal') closeAuth();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to features
|
||||||
|
function scrollToFeatures() {
|
||||||
|
document.getElementById('features').scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle login
|
||||||
|
async function handleLogin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const email = document.getElementById('loginEmail').value;
|
||||||
|
const password = document.getElementById('loginPassword').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(API.login, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
authToken = json.token;
|
||||||
|
localStorage.setItem('vibechat_token', authToken);
|
||||||
|
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
||||||
|
window.location.href = 'app.html';
|
||||||
|
} else {
|
||||||
|
alert('Ошибка: ' + json.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка подключения: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle register
|
||||||
|
async function handleRegister(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('regUsername').value;
|
||||||
|
const email = document.getElementById('regEmail').value;
|
||||||
|
const password = document.getElementById('regPassword').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(API.register, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.ok) {
|
||||||
|
authToken = json.token;
|
||||||
|
localStorage.setItem('vibechat_token', authToken);
|
||||||
|
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
||||||
|
window.location.href = 'app.html';
|
||||||
|
} else {
|
||||||
|
alert('Ошибка: ' + json.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка подключения: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already logged in
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const token = localStorage.getItem('vibechat_token');
|
||||||
|
if (token) {
|
||||||
|
window.location.href = 'app.html';
|
||||||
|
}
|
||||||
|
});
|
||||||
10
package-lock.json
generated
Normal file
10
package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "apple-style-app",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "apple-style-app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "apple-style-app",
|
"name": "vibechat-messenger",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
}
|
},
|
||||||
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
359
server.js
359
server.js
@ -1,12 +1,14 @@
|
|||||||
const http = require("http");
|
const http = require("http");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const DATA = path.join(__dirname, "data.json");
|
const DATA = path.join(__dirname, "data.json");
|
||||||
|
const SECRET_KEY = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
function readData() {
|
function readData() {
|
||||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { products: [], announcements: [], siteSettings: {} }; }
|
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { users: [], chats: [], messages: [], groups: [], channels: [], sessions: [] }; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeData(data) {
|
function writeData(data) {
|
||||||
@ -31,93 +33,321 @@ function notFound(res) {
|
|||||||
res.end("Not Found");
|
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 = {
|
const routes = {
|
||||||
// PUBLIC API
|
// ========== AUTH ==========
|
||||||
"GET /api/products": (req, res) => {
|
"POST /api/auth/register": async (req, res) => {
|
||||||
const data = readData();
|
const { username, email, password } = await parseBody(req);
|
||||||
json(res, { ok: true, data: data.products.sort((a, b) => a.order - b.order) });
|
if (!username || !email || !password) return json(res, { ok: false, error: "Все поля обязательны" }, 400);
|
||||||
},
|
|
||||||
|
|
||||||
"GET /api/products/:id": (req, res, params) => {
|
|
||||||
const data = readData();
|
const data = readData();
|
||||||
const product = data.products.find(p => p.id === parseInt(params.id));
|
if (data.users.find(u => u.email === email)) return json(res, { ok: false, error: "Email уже зарегистрирован" }, 400);
|
||||||
if (!product) return json(res, { ok: false, error: "Product not found" }, 404);
|
if (data.users.find(u => u.username === username)) return json(res, { ok: false, error: "Имя пользователя занято" }, 400);
|
||||||
json(res, { ok: true, data: product });
|
|
||||||
},
|
|
||||||
|
|
||||||
"GET /api/announcements": (req, res) => {
|
const user = {
|
||||||
const data = readData();
|
|
||||||
json(res, { ok: true, data: data.announcements.filter(a => a.active) });
|
|
||||||
},
|
|
||||||
|
|
||||||
"GET /api/settings": (req, res) => {
|
|
||||||
const data = readData();
|
|
||||||
json(res, { ok: true, data: data.siteSettings });
|
|
||||||
},
|
|
||||||
|
|
||||||
// ADMIN API
|
|
||||||
"POST /api/admin/products": async (req, res) => {
|
|
||||||
const body = await parseBody(req);
|
|
||||||
const data = readData();
|
|
||||||
const newProduct = {
|
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
...body,
|
username,
|
||||||
order: data.products.length + 1
|
email,
|
||||||
|
passwordHash: hashPassword(password),
|
||||||
|
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`,
|
||||||
|
bio: "",
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
data.products.push(newProduct);
|
|
||||||
|
data.users.push(user);
|
||||||
writeData(data);
|
writeData(data);
|
||||||
json(res, { ok: true, data: newProduct }, 201);
|
|
||||||
|
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 } });
|
||||||
},
|
},
|
||||||
|
|
||||||
"PUT /api/admin/products/:id": async (req, res, params) => {
|
"POST /api/auth/login": async (req, res) => {
|
||||||
const body = await parseBody(req);
|
const { email, password } = await parseBody(req);
|
||||||
|
if (!email || !password) return json(res, { ok: false, error: "Email и пароль обязательны" }, 400);
|
||||||
|
|
||||||
const data = readData();
|
const data = readData();
|
||||||
const idx = data.products.findIndex(p => p.id === parseInt(params.id));
|
const user = data.users.find(u => u.email === email && u.passwordHash === hashPassword(password));
|
||||||
if (idx === -1) return json(res, { ok: false, error: "Product not found" }, 404);
|
if (!user) return json(res, { ok: false, error: "Неверный email или пароль" }, 401);
|
||||||
data.products[idx] = { ...data.products[idx], ...body, id: parseInt(params.id) };
|
|
||||||
|
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);
|
writeData(data);
|
||||||
json(res, { ok: true, data: data.products[idx] });
|
|
||||||
|
json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } });
|
||||||
},
|
},
|
||||||
|
|
||||||
"DELETE /api/admin/products/:id": (req, res, params) => {
|
"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();
|
const data = readData();
|
||||||
data.products = data.products.filter(p => p.id !== parseInt(params.id));
|
data.sessions = data.sessions.filter(s => s.token !== token);
|
||||||
writeData(data);
|
writeData(data);
|
||||||
json(res, { ok: true, deleted: parseInt(params.id) });
|
json(res, { ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"POST /api/admin/announcements": async (req, res) => {
|
"GET /api/auth/me": async (req, res) => {
|
||||||
const body = await parseBody(req);
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
const data = readData();
|
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||||
const newAnnouncement = { id: Date.now(), ...body, active: true };
|
|
||||||
data.announcements.push(newAnnouncement);
|
const user = verifyToken(token);
|
||||||
writeData(data);
|
if (!user) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||||
json(res, { ok: true, data: newAnnouncement }, 201);
|
|
||||||
|
json(res, { ok: true, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar, bio: user.bio } });
|
||||||
},
|
},
|
||||||
|
|
||||||
"PUT /api/admin/announcements/:id": async (req, res, params) => {
|
// ========== USERS ==========
|
||||||
const body = await parseBody(req);
|
"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 data = readData();
|
||||||
const idx = data.announcements.findIndex(a => a.id === parseInt(params.id));
|
const users = data.users.filter(u => u.id !== currentUser.id).map(u => ({
|
||||||
if (idx === -1) return json(res, { ok: false, error: "Not found" }, 404);
|
id: u.id, username: u.username, avatar: u.avatar
|
||||||
data.announcements[idx] = { ...data.announcements[idx], ...body, id: parseInt(params.id) };
|
}));
|
||||||
writeData(data);
|
json(res, { ok: true, users });
|
||||||
json(res, { ok: true, data: data.announcements[idx] });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"DELETE /api/admin/announcements/:id": (req, res, params) => {
|
"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 data = readData();
|
||||||
data.announcements = data.announcements.filter(a => a.id !== parseInt(params.id));
|
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);
|
writeData(data);
|
||||||
json(res, { ok: true, deleted: parseInt(params.id) });
|
|
||||||
|
json(res, { ok: true, user: data.users[idx] });
|
||||||
},
|
},
|
||||||
|
|
||||||
"PUT /api/admin/settings": async (req, res) => {
|
// ========== CHATS ==========
|
||||||
const body = await parseBody(req);
|
"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 data = readData();
|
||||||
data.siteSettings = { ...data.siteSettings, ...body };
|
const userChats = data.chats.filter(c => c.participants.includes(currentUser.id));
|
||||||
|
|
||||||
|
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);
|
writeData(data);
|
||||||
json(res, { ok: true, data: data.siteSettings });
|
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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -125,10 +355,7 @@ const mimeTypes = {
|
|||||||
".html": "text/html; charset=utf-8",
|
".html": "text/html; charset=utf-8",
|
||||||
".css": "text/css; charset=utf-8",
|
".css": "text/css; charset=utf-8",
|
||||||
".js": "application/javascript; charset=utf-8",
|
".js": "application/javascript; charset=utf-8",
|
||||||
".json": "application/json; charset=utf-8",
|
".json": "application/json; charset=utf-8"
|
||||||
".png": "image/png",
|
|
||||||
".jpg": "image/jpeg",
|
|
||||||
".svg": "image/svg+xml"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
http.createServer(async (req, res) => {
|
http.createServer(async (req, res) => {
|
||||||
@ -137,7 +364,7 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
|
||||||
if (method === "OPTIONS") {
|
if (method === "OPTIONS") {
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
@ -182,4 +409,4 @@ http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notFound(res);
|
notFound(res);
|
||||||
}).listen(PORT, () => console.log("Server running on port " + PORT));
|
}).listen(PORT, () => console.log("VibeChat server running on port " + PORT));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user